diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index efcc6e4c476..2084f16004f 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -76,3 +76,7 @@ harness = false name = "char_cell_bench" harness = false required-features = ["test-util"] + +[[bench]] +name = "text_layout_bench" +harness = false diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs new file mode 100644 index 00000000000..13dc0b1d0e1 --- /dev/null +++ b/crates/editor/benches/text_layout_bench.rs @@ -0,0 +1,227 @@ +use std::hint::black_box; +use std::sync::Arc; +use std::time::Duration; + +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use rayon::ThreadPoolBuilder; +use string_offset::CharOffset; +use warp_editor::content::buffer::{StyledBufferBlock, StyledBufferRun, StyledTextBlock}; +use warp_editor::content::edit::EditDelta; +use warp_editor::content::text::{BufferBlockStyle, TextStylesWithMetadata}; +use warp_editor::render::layout::TextLayout; +use warp_editor::render::model::{ + BrokenLinkStyle, CheckBoxStyle, HorizontalRuleStyle, InlineCodeStyle, ParagraphStyles, + RenderLayoutOptions, RichTextStyles, TableStyle, +}; +#[cfg(target_os = "macos")] +use warpui::platform::mac::FontDB as MacFontDB; +use warpui_core::App; +use warpui_core::color::ColorU; +use warpui_core::elements::{Border, Fill}; +#[cfg(target_os = "macos")] +use warpui_core::fonts::Cache as FontCache; +use warpui_core::fonts::{FamilyId, Weight}; +use warpui_core::units::IntoPixels; + +const BLOCK_COUNT: usize = 4_096; +const RAYON_THREAD_COUNT: usize = 6; +const SHAPING_TEXT: &str = concat!( + "office affinity efficient waffle: ffi ffl fi fl; ", + "English left-to-right text surrounds العربية: السَّلَامُ عَلَيْكُمْ ورحمة الله, ", + "then עברית: שלום עולם and mixed account-42 مرحبا status=ready; ", + "combining marks: café naïve coöperate Ångström; ", + "Devanagari: नमस्ते दुनिया; Thai: สวัสดีชาวโลก; ", + "emoji and joiners: 👩‍💻 👨‍👩‍👧‍👦; " +); + +fn benchmark_block(text: String) -> StyledBufferBlock { + let content_length = text.chars().count(); + StyledBufferBlock::Text(StyledTextBlock { + block: vec![StyledBufferRun { + run: text, + text_styles: TextStylesWithMetadata::default(), + block_style: BufferBlockStyle::PlainText, + }], + style: BufferBlockStyle::PlainText, + content_length: CharOffset::from(content_length), + }) +} + +fn benchmark_styles(font_family: FamilyId) -> RichTextStyles { + let white = ColorU::white(); + let paragraph = ParagraphStyles { + font_family, + font_size: 13., + font_weight: Weight::Normal, + line_height_ratio: 1.2, + text_color: white, + baseline_ratio: 0.8, + fixed_width_tab_size: None, + }; + RichTextStyles { + base_text: paragraph, + code_text: ParagraphStyles { + fixed_width_tab_size: Some(4), + ..paragraph + }, + code_background: Fill::None, + embedding_background: Fill::None, + embedding_text: paragraph, + code_border: Border::new(0.), + placeholder_color: white, + selection_fill: Fill::None, + cursor_fill: Fill::None, + inline_code_style: InlineCodeStyle { + font_family, + background: white, + font_color: white, + }, + check_box_style: CheckBoxStyle { + border_width: 2., + border_color: white, + icon_path: "bundled/svg/check-thick.svg", + background: white, + hover_background: white, + }, + horizontal_rule_style: HorizontalRuleStyle { + rule_height: 2., + color: white, + }, + broken_link_style: BrokenLinkStyle { + icon_path: "bundled/svg/link-broken-02.svg", + icon_color: white, + }, + block_spacings: Default::default(), + minimum_paragraph_height: Some(24.0.into_pixels()), + show_placeholder_text_on_empty_block: false, + cursor_width: 1., + highlight_urls: false, + table_style: TableStyle { + border_color: white, + header_background: white, + cell_background: white, + alternate_row_background: None, + text_color: white, + header_text_color: white, + scrollbar_nonactive_thumb_color: white, + scrollbar_active_thumb_color: white, + font_family, + font_size: 13., + cell_padding: 8., + outer_border: true, + column_dividers: true, + row_dividers: true, + }, + } +} + +fn benchmark_delta(texts: impl IntoIterator) -> (EditDelta, usize) { + let blocks: Vec<_> = texts.into_iter().map(benchmark_block).collect(); + let chars = blocks + .iter() + .map(StyledBufferBlock::content_length) + .map(CharOffset::as_usize) + .sum(); + ( + EditDelta { + old_offset: CharOffset::from(1)..CharOffset::from(1 + chars), + new_lines: Arc::new(blocks), + ..Default::default() + }, + chars, + ) +} + +fn layout_delta( + delta: &EditDelta, + text_layout: &TextLayout<'_>, + layout_options: &RenderLayoutOptions, + app: &warpui_core::AppContext, +) { + black_box(delta.layout_delta(text_layout, None, layout_options, None, app)); +} + +fn text_layout_benchmarks(criterion: &mut Criterion) { + ThreadPoolBuilder::new() + .num_threads(RAYON_THREAD_COUNT) + .build_global() + .expect("benchmark should initialize Rayon before first use"); + let (test_delta, test_chars) = benchmark_delta( + (0..BLOCK_COUNT).map(|index| format!("{SHAPING_TEXT} test-backend-block-{index:04}\n")), + ); + let test_styles = benchmark_styles(FamilyId(0)); + let layout_options = RenderLayoutOptions::default(); + #[cfg(target_os = "macos")] + let (core_text_font_cache, core_text_styles, core_text_delta, core_text_chars) = { + let mut font_cache = FontCache::new(Box::new(MacFontDB::new())); + let font_family = font_cache + .load_system_font("Menlo") + .expect("Menlo should be available on macOS"); + let (delta, chars) = benchmark_delta( + (0..BLOCK_COUNT).map(|index| format!("{SHAPING_TEXT} core-text-block-{index:04}\n")), + ); + (font_cache, benchmark_styles(font_family), delta, chars) + }; + let mut criterion = std::mem::take(criterion); + + App::test((), move |app| async move { + app.read(|ctx| { + let test_text_layout = TextLayout::new( + ctx.font_cache().text_layout_system(), + &test_styles, + f32::MAX, + ); + layout_delta(&test_delta, &test_text_layout, &layout_options, ctx); + #[cfg(target_os = "macos")] + { + let core_text_layout = TextLayout::new( + core_text_font_cache.text_layout_system(), + &core_text_styles, + f32::MAX, + ); + layout_delta(&core_text_delta, &core_text_layout, &layout_options, ctx); + } + { + let mut group = criterion.benchmark_group("editor_text_layout/test_backend"); + group.throughput(Throughput::Elements(test_chars as u64)); + group.bench_function("layout_delta_4096_shaping_blocks_6_threads", |bench| { + bench.iter(|| { + let text_layout = TextLayout::new( + ctx.font_cache().text_layout_system(), + &test_styles, + f32::MAX, + ); + layout_delta(&test_delta, &text_layout, &layout_options, ctx) + }) + }); + group.finish(); + } + #[cfg(target_os = "macos")] + { + let mut group = criterion.benchmark_group("editor_text_layout/core_text"); + group.throughput(Throughput::Elements(core_text_chars as u64)); + group.bench_function("layout_delta_4096_shaping_blocks_6_threads", |bench| { + bench.iter(|| { + let text_layout = TextLayout::new( + core_text_font_cache.text_layout_system(), + &core_text_styles, + f32::MAX, + ); + layout_delta(&core_text_delta, &text_layout, &layout_options, ctx) + }) + }); + group.finish(); + } + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(20) + .warm_up_time(Duration::from_secs(2)) + .measurement_time(Duration::from_secs(5)); + targets = text_layout_benchmarks +} +criterion_main!(benches); diff --git a/crates/editor/src/content/edit_tests.rs b/crates/editor/src/content/edit_tests.rs index 552d4791bcd..9be6cdff66c 100644 --- a/crates/editor/src/content/edit_tests.rs +++ b/crates/editor/src/content/edit_tests.rs @@ -11,7 +11,7 @@ use warp_core::features::FeatureFlag; use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState}; use warpui_core::fonts::{Properties, Style, Weight}; use warpui_core::image_cache::ImageType; -use warpui_core::text_layout::{LayoutCache, StyleAndFont, TextStyle}; +use warpui_core::text_layout::{StyleAndFont, TextStyle}; use warpui_core::{App, SingletonEntity}; use super::{ @@ -243,9 +243,7 @@ fn test_layout_delta_never_takes_ownership_of_new_lines_with_multiple_owners() { // layout call touches the `Arc`'s strong count or invalidates the other clone. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -310,8 +308,6 @@ fn test_layout_delta_never_takes_ownership_of_new_lines_with_multiple_owners() { fn test_layout_partial_url() { // Regression test for laying out a partially-styled autodetected URL (CLD-871). App::test((), |app| async move { - let layout_cache = LayoutCache::new(); - let runs = vec![ StyledBufferRun { run: "A link: https://www.".to_string(), @@ -332,7 +328,6 @@ fn test_layout_partial_url() { app.read(|ctx| { let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -399,13 +394,8 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() { } app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block_style = BufferBlockStyle::CodeBlock { code_block_type: CodeBlockType::Mermaid, }; @@ -476,13 +466,8 @@ fn test_unloaded_mermaid_diagram_uses_stable_full_width_placeholder_height() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let contents = "graph TD\nA[Unloaded] --> B[Placeholder]\n"; let block_style = BufferBlockStyle::CodeBlock { code_block_type: CodeBlockType::Mermaid, @@ -536,13 +521,8 @@ fn test_empty_mermaid_block_lays_out_as_code_block() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block = mermaid_code_block("\n"); let (item, _has_trailing_newline) = layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx) @@ -571,13 +551,8 @@ fn test_non_parseable_mermaid_block_lays_out_as_code_block() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block = mermaid_code_block("echo hi\n"); let (item, _) = layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx) @@ -634,13 +609,8 @@ fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() { "Mermaid render for invalid source should have failed" ); - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block = mermaid_code_block(contents); let (item, _) = layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx) @@ -686,13 +656,8 @@ fn test_valid_mermaid_block_lays_out_as_diagram_after_load() { } app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block = mermaid_code_block(contents); let (item, _) = layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx) @@ -711,13 +676,8 @@ fn test_mermaid_block_skipped_when_render_disabled() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); app.read(|ctx| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let block = mermaid_code_block("graph TD\nA --> B\n"); let options = RenderLayoutOptions { render_mermaid_diagrams: false, @@ -818,9 +778,7 @@ fn test_layout_text_block_uses_rich_table_when_flag_enabled() { App::test((), |app| async move { app.read(|ctx| { let _flag = FeatureFlag::MarkdownTables.override_enabled(true); - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -851,9 +809,7 @@ fn test_layout_text_block_uses_plain_text_when_flag_disabled() { App::test((), |app| async move { app.read(|ctx| { let _flag = FeatureFlag::MarkdownTables.override_enabled(false); - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -882,9 +838,7 @@ fn test_layout_text_block_uses_plain_text_when_flag_disabled() { fn test_layout_table_block_caches_cell_text_frames() { App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -931,9 +885,7 @@ fn test_layout_table_block_caches_cell_text_frames() { fn test_layout_table_block_clamps_cell_width_to_max() { App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -991,10 +943,8 @@ fn test_layout_table_block_clamps_cell_width_to_max() { #[test] fn test_table_inline_style_runs_apply_header_bold_default() { App::test((), |app| async move { - let layout_cache = LayoutCache::new(); app.read(|ctx| { let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1029,10 +979,8 @@ fn test_table_inline_style_runs_apply_header_bold_default() { #[test] fn test_table_inline_style_runs_preserve_markdown_cell_styles() { App::test((), |app| async move { - let layout_cache = LayoutCache::new(); app.read(|ctx| { let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1110,9 +1058,7 @@ fn test_layout_code_block_urls() { ]; app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1258,9 +1204,7 @@ fn test_layout_delta_chunk_boundary_preserves_order_hidden_collapsing_and_traili // MAX_LAYOUT_TASKS_PER_PARALLEL_CHUNK), with a hidden run that straddles a chunk boundary. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1353,9 +1297,7 @@ fn test_layout_delta_single_chunk_matches_direct_layout() { // ends in one. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1436,9 +1378,7 @@ fn test_layout_delta_block_location_is_global_across_chunk_boundaries() { // makes a chunk-local-index regression directly observable. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1535,9 +1475,7 @@ fn test_layout_delta_trailing_newline_carries_over_when_final_chunk_fully_fails( // contributed nothing. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, @@ -1614,9 +1552,7 @@ fn test_layout_temporary_blocks_preserves_order_across_chunk_boundary() { // their original order. App::test((), |app| async move { app.read(|ctx| { - let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( - &layout_cache, ctx.font_cache().text_layout_system(), &TEST_STYLES, f32::MAX, diff --git a/crates/editor/src/content/mermaid_diagram_tests.rs b/crates/editor/src/content/mermaid_diagram_tests.rs index 95338131caa..517952c74b4 100644 --- a/crates/editor/src/content/mermaid_diagram_tests.rs +++ b/crates/editor/src/content/mermaid_diagram_tests.rs @@ -1,6 +1,5 @@ use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState}; use warpui_core::image_cache::ImageType; -use warpui_core::text_layout::LayoutCache; use warpui_core::{App, SingletonEntity}; use super::*; @@ -20,13 +19,8 @@ fn loading_mermaid_layout_uses_default_height() { App::test((), |app| async move { app.read(|ctx| { let source = "graph TD\nA[Start] --> B[Finish]\n"; - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let (_asset_source, config) = mermaid_diagram_layout(source, &text_layout, mermaid_block_spacing(), ctx); let expected_height = TEST_STYLES.base_line_height() @@ -84,13 +78,8 @@ fn failed_mermaid_layout_uses_compact_height() { AssetState::FailedToLoad(_) )); - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &TEST_STYLES, - 800., - ); + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &TEST_STYLES, 800.); let config = mermaid_diagram_config(&asset_source, &text_layout, mermaid_block_spacing(), ctx); let expected_height = TEST_STYLES.base_line_height() diff --git a/crates/editor/src/render/element/empty.rs b/crates/editor/src/render/element/empty.rs index bf4ce899943..6c0f585c8d4 100644 --- a/crates/editor/src/render/element/empty.rs +++ b/crates/editor/src/render/element/empty.rs @@ -34,13 +34,16 @@ impl RenderableBlock for Empty { ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |_| { - placeholder::Options { - text: paragraph_placeholder_text(model.selections().len() == 1), - block_style: BufferBlockStyle::PlainText, - } - }); + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |_| placeholder::Options { + text: paragraph_placeholder_text(model.selections().len() == 1), + block_style: BufferBlockStyle::PlainText, + }, + ); } fn paint( diff --git a/crates/editor/src/render/element/header.rs b/crates/editor/src/render/element/header.rs index 44a0fa6a179..e1ee9c1a9d4 100644 --- a/crates/editor/src/render/element/header.rs +++ b/crates/editor/src/render/element/header.rs @@ -30,8 +30,12 @@ impl RenderableBlock for RenderableHeader { ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |block| { let header_size = match block { BlockItem::Header { header_size, .. } => *header_size, other => { @@ -46,7 +50,8 @@ impl RenderableBlock for RenderableHeader { text: header_size.label(), block_style: BufferBlockStyle::Header { header_size }, } - }); + }, + ); } fn paint( diff --git a/crates/editor/src/render/element/ordered_list.rs b/crates/editor/src/render/element/ordered_list.rs index 9a14c5a04a4..90312507bf0 100644 --- a/crates/editor/src/render/element/ordered_list.rs +++ b/crates/editor/src/render/element/ordered_list.rs @@ -43,7 +43,7 @@ impl RenderableBlock for RenderableOrderedListItem { ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - let text_layout = TextLayout::from_layout_context(ctx, app, model); + let text_layout = TextLayout::for_render_state(app, model); let block_style = BufferBlockStyle::OrderedList { indent_level: ListIndentLevel::One, number: None, @@ -62,13 +62,16 @@ impl RenderableBlock for RenderableOrderedListItem { style_runs, )); - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |_| { - placeholder::Options { - block_style, - text: "List", - } - }); + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |_| placeholder::Options { + block_style, + text: "List", + }, + ); } fn paint( diff --git a/crates/editor/src/render/element/paragraph.rs b/crates/editor/src/render/element/paragraph.rs index 5f854655a38..e4853857927 100644 --- a/crates/editor/src/render/element/paragraph.rs +++ b/crates/editor/src/render/element/paragraph.rs @@ -46,13 +46,16 @@ impl RenderableBlock for RenderableParagraph { ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |_| { - placeholder::Options { - text: paragraph_placeholder_text(model.selections().len() == 1), - block_style: BufferBlockStyle::PlainText, - } - }); + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |_| placeholder::Options { + text: paragraph_placeholder_text(model.selections().len() == 1), + block_style: BufferBlockStyle::PlainText, + }, + ); } fn paint( diff --git a/crates/editor/src/render/element/placeholder.rs b/crates/editor/src/render/element/placeholder.rs index 1112d00e105..f28fa667192 100644 --- a/crates/editor/src/render/element/placeholder.rs +++ b/crates/editor/src/render/element/placeholder.rs @@ -1,8 +1,8 @@ use std::sync::Arc; +use warpui_core::AppContext; use warpui_core::geometry::vector::{Vector2F, vec2f}; -use warpui_core::text_layout::Line; -use warpui_core::{AppContext, LayoutContext}; +use warpui_core::text_layout::{LayoutCache, Line}; use super::{CursorData, RenderContext}; use crate::content::text::BufferBlockStyle; @@ -42,7 +42,7 @@ impl BlockPlaceholder { &mut self, item: &ViewportItem, model: &RenderState, - ctx: &mut LayoutContext, + layout_cache: &LayoutCache, app: &AppContext, options: F, ) where @@ -77,10 +77,15 @@ impl BlockPlaceholder { return; } - let layout = TextLayout::from_layout_context(ctx, app, model); + let layout = TextLayout::for_render_state(app, model); let options = options(block.item); self.state = State::LaidOut { - line: layout.layout_placeholder(options.text, &options.block_style, &item.spacing), + line: layout.layout_placeholder( + layout_cache, + options.text, + &options.block_style, + &item.spacing, + ), block_style: options.block_style, contains_cursor, }; diff --git a/crates/editor/src/render/element/task_list.rs b/crates/editor/src/render/element/task_list.rs index 558e9cc0ece..2aa6b97a68a 100644 --- a/crates/editor/src/render/element/task_list.rs +++ b/crates/editor/src/render/element/task_list.rs @@ -114,26 +114,29 @@ impl RenderableBlock for RenderableTaskList { ctx, app, ); - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { - placeholder::Options { - text: "To-do list", - block_style: match block { - BlockItem::TaskList { - indent_level, - complete, - .. - } => BufferBlockStyle::TaskList { - indent_level: *indent_level, - complete: *complete, - }, - _ => BufferBlockStyle::TaskList { - indent_level: ListIndentLevel::One, - complete: false, - }, + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |block| placeholder::Options { + text: "To-do list", + block_style: match block { + BlockItem::TaskList { + indent_level, + complete, + .. + } => BufferBlockStyle::TaskList { + indent_level: *indent_level, + complete: *complete, + }, + _ => BufferBlockStyle::TaskList { + indent_level: ListIndentLevel::One, + complete: false, }, - } - }) + }, + }, + ) } fn paint( diff --git a/crates/editor/src/render/element/unordered_list.rs b/crates/editor/src/render/element/unordered_list.rs index f3314cb98fd..213e9163f60 100644 --- a/crates/editor/src/render/element/unordered_list.rs +++ b/crates/editor/src/render/element/unordered_list.rs @@ -69,8 +69,12 @@ impl RenderableBlock for RenderableBulletList { ctx, app, ); - self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { + self.placeholder.layout( + &self.viewport_item, + model, + ctx.text_layout_cache, + app, + |block| { let indent_level = match block { BlockItem::UnorderedList { indent_level, .. } => *indent_level, _ => ListIndentLevel::One, @@ -79,7 +83,8 @@ impl RenderableBlock for RenderableBulletList { text: "List", block_style: BufferBlockStyle::UnorderedList { indent_level }, } - }) + }, + ) } fn paint( diff --git a/crates/editor/src/render/layout.rs b/crates/editor/src/render/layout.rs index e892081f7d5..a17ba62aa1b 100644 --- a/crates/editor/src/render/layout.rs +++ b/crates/editor/src/render/layout.rs @@ -5,6 +5,7 @@ use std::sync::Arc; #[cfg(test)] use markdown_parser::FormattedTextInline; +use warpui_core::AppContext; use warpui_core::color::ColorU; use warpui_core::fonts::TextLayoutSystem; #[cfg(test)] @@ -13,7 +14,6 @@ use warpui_core::text_layout::{ ClipConfig, LayoutCache, Line, StyleAndFont, TextAlignment, TextBorder, TextFrame, TextStyle, }; use warpui_core::units::{IntoPixels, Pixels}; -use warpui_core::{AppContext, LayoutContext}; use super::model::{BlockSpacing, ParagraphStyles, RenderState, RichTextStyles}; use crate::content::text::{BufferBlockStyle, TextStylesWithMetadata}; @@ -68,7 +68,6 @@ pub(crate) struct InlineTextLayoutInput { /// Utility for laying out rich text. pub struct TextLayout<'a> { - layout_cache: &'a LayoutCache, font_cache: TextLayoutSystem<'a>, rich_text_styles: &'a RichTextStyles, max_width: f32, @@ -77,13 +76,11 @@ pub struct TextLayout<'a> { impl<'a> TextLayout<'a> { pub fn new( - layout_cache: &'a LayoutCache, font_cache: TextLayoutSystem<'a>, rich_text_styles: &'a RichTextStyles, max_width: f32, ) -> Self { Self { - layout_cache, font_cache, rich_text_styles, max_width, @@ -106,14 +103,9 @@ impl<'a> TextLayout<'a> { self.container_scrolls_horizontally } - /// Builds a [`TextLayout`] from the context passed to `Element::layout`. - pub fn from_layout_context( - ctx: &LayoutContext<'a>, - app: &'a AppContext, - model: &'a RenderState, - ) -> Self { + /// Builds a [`TextLayout`] for an editor render state. + pub fn for_render_state(app: &'a AppContext, model: &'a RenderState) -> Self { Self::new( - ctx.text_layout_cache, app.font_cache().text_layout_system(), model.styles(), model.viewport().width().as_f32(), @@ -162,7 +154,7 @@ impl<'a> TextLayout<'a> { clamp_style_runs_for_layout(style_runs) }); - self.layout_cache.layout_text( + Arc::new(self.font_cache.layout_text_uncached( shaped_text, paragraph_style.line_style(), clamped_style_runs.as_deref().unwrap_or(style_runs), @@ -170,13 +162,13 @@ impl<'a> TextLayout<'a> { f32::MAX, alignment, None, - &self.font_cache, - ) + )) } /// Lays out placeholder text for empty blocks. pub fn layout_placeholder( &self, + layout_cache: &LayoutCache, text: &str, block_type: &BufferBlockStyle, spacing: &BlockSpacing, @@ -188,7 +180,7 @@ impl<'a> TextLayout<'a> { ); let text = truncate_text_for_layout(text); let style_runs = &[(0..text.chars().count(), style_and_font)]; - self.layout_cache.layout_line( + layout_cache.layout_line( text, paragraph_styles.line_style(), style_runs, diff --git a/crates/editor/src/render/model/mod.rs b/crates/editor/src/render/model/mod.rs index 2bc7c3d0ab3..0b9020d4be4 100644 --- a/crates/editor/src/render/model/mod.rs +++ b/crates/editor/src/render/model/mod.rs @@ -35,7 +35,7 @@ use warpui_core::fonts::{FamilyId, Properties, Weight}; use warpui_core::geometry::rect::RectF; use warpui_core::geometry::vector::{Vector2F, vec2f}; use warpui_core::platform::LineStyle; -use warpui_core::text_layout::{CaretPosition, LayoutCache, Line, TextFrame}; +use warpui_core::text_layout::{CaretPosition, Line, TextFrame}; use warpui_core::text_selection_utils::{ NewlineTickParams, calculate_tick_width, create_newline_tick_rect, selection_crosses_newline_offset_based, @@ -1420,7 +1420,7 @@ impl ColumnUnit { } /// A character offset within a [`TextFrame`]. These offsets count characters in the Rust string -/// passed to [`warpui_core::text_layout::LayoutCache::layout_text()`]. +/// passed to the text layout system. /// /// Frame offsets often, but not always, correspond to glyph indices and caret positions. However, /// they do not line up 1:1 if a glyph or grapheme contains multiple characters @@ -3374,8 +3374,7 @@ impl RenderState { } fn layout_temporary_blocks(&self, blocks: Vec, app: &AppContext) { - let layout_cache = LayoutCache::new(); - let layout_context = self.layout_context(&layout_cache, app); + let layout_context = self.layout_context(app); let laid_out_blocks = layout_temporary_blocks(blocks, &layout_context); self.reset_temporary_block(laid_out_blocks); } @@ -3386,8 +3385,7 @@ impl RenderState { hidden_ranges: Option>, app: &AppContext, ) { - let layout_cache = LayoutCache::new(); - let layout_context = self.layout_context(&layout_cache, app); + let layout_context = self.layout_context(app); let laid_out_edit = delta.layout_delta( &layout_context, self.document_path.as_deref(), @@ -3398,15 +3396,8 @@ impl RenderState { self.layout_pending_edit(laid_out_edit, hidden_ranges); } - /// Construct a throwaway layout cache. We only lay out modified text, so in effect, - /// the entire RenderState is a cache. - fn layout_context<'a>( - &'a self, - layout_cache: &'a LayoutCache, - ctx: &'a AppContext, - ) -> TextLayout<'a> { + fn layout_context<'a>(&'a self, ctx: &'a AppContext) -> TextLayout<'a> { TextLayout::new( - layout_cache, ctx.font_cache().text_layout_system(), &self.styles, match self.width_setting { diff --git a/crates/editor/src/render/model/offset_map_tests.rs b/crates/editor/src/render/model/offset_map_tests.rs index f3a8a1721e1..7af6402947a 100644 --- a/crates/editor/src/render/model/offset_map_tests.rs +++ b/crates/editor/src/render/model/offset_map_tests.rs @@ -81,7 +81,6 @@ fn test_end_to_end() { use warpui_core::color::ColorU; use warpui_core::elements::Fill; use warpui_core::fonts::Cache as FontCache; - use warpui_core::text_layout::LayoutCache; use crate::content::buffer::{Buffer, BufferEditAction, EditOrigin}; use crate::content::selection_model::BufferSelectionModel; @@ -95,7 +94,6 @@ fn test_end_to_end() { App::test((), |mut app| async move { let mut font_cache = FontCache::new(Box::new(warpui::platform::current::FontDB::new())); - let layout_cache = LayoutCache::new(); let paragraph_styles = ParagraphStyles { font_family: font_cache .load_system_font("Arial") @@ -202,12 +200,7 @@ fn test_end_to_end() { // Now, lay out the buffer, which should produce a single `Paragraph` block. let layout = app.read(|ctx| { let delta = buffer_handle.as_ref(ctx).invalidate_layout(); - let text_layout = TextLayout::new( - &layout_cache, - font_cache.text_layout_system(), - &styles, - 1000., - ); + let text_layout = TextLayout::new(font_cache.text_layout_system(), &styles, 1000.); delta.layout_delta( &text_layout, None, diff --git a/crates/warpui_core/src/core/app.rs b/crates/warpui_core/src/core/app.rs index 995d27a1e21..42837d1897a 100644 --- a/crates/warpui_core/src/core/app.rs +++ b/crates/warpui_core/src/core/app.rs @@ -3782,6 +3782,7 @@ impl AppContext { .remove_text_frame(&key); } } + RequestedFallbackFontSource::UncachedText => {} } } diff --git a/crates/warpui_core/src/fonts/external_fallback.rs b/crates/warpui_core/src/fonts/external_fallback.rs index 4a8208ae629..af7b8da92db 100644 --- a/crates/warpui_core/src/fonts/external_fallback.rs +++ b/crates/warpui_core/src/fonts/external_fallback.rs @@ -33,6 +33,7 @@ pub(crate) enum RequestedFallbackFontSource { GlyphForChar((FontId, char)), Line(text_layout::CacheKeyValue), TextFrame(text_layout::CacheKeyValue), + UncachedText, } pub(crate) struct FontBytes(pub Vec); diff --git a/crates/warpui_core/src/fonts/text_layout_system.rs b/crates/warpui_core/src/fonts/text_layout_system.rs index a5ab24ba07d..9dd3a36aae0 100644 --- a/crates/warpui_core/src/fonts/text_layout_system.rs +++ b/crates/warpui_core/src/fonts/text_layout_system.rs @@ -3,7 +3,9 @@ use std::ops::Range; use crate::fonts::{FontFallbackCache, RequestedFallbackFontSource}; use crate::platform; use crate::platform::LineStyle; -use crate::text_layout::{ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame}; +use crate::text_layout::{ + ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame, strip_leading_unicode_bom, +}; /// Struct to layout text, updating cached font fallback state as needed. /// See [fonts::Cache::text_layout_system]. @@ -57,4 +59,41 @@ impl TextLayoutSystem<'_> { first_line_head_indent, ) } + + /// Lays out a text frame without retaining it in a [`crate::text_layout::LayoutCache`]. + #[allow(clippy::too_many_arguments)] + pub fn layout_text_uncached( + &self, + text: &str, + line_style: LineStyle, + style_runs: &[(Range, StyleAndFont)], + max_width: f32, + max_height: f32, + alignment: TextAlignment, + first_line_head_indent: Option, + ) -> TextFrame { + let (text, adjusted_style_runs) = strip_leading_unicode_bom(text, style_runs); + let style_runs = adjusted_style_runs + .as_ref() + .map_or(style_runs, Vec::as_slice); + let text_frame = self.layout_text( + text, + line_style, + style_runs, + max_width, + max_height, + alignment, + first_line_head_indent, + ); + for line in text_frame.lines() { + self.request_fallback_fonts(&line.chars_with_missing_glyphs); + } + text_frame + } + + fn request_fallback_fonts(&self, missing_glyphs: &[char]) { + for ch in missing_glyphs { + self.request_fallback_font_for_char(*ch, RequestedFallbackFontSource::UncachedText); + } + } } diff --git a/crates/warpui_core/src/text_layout.rs b/crates/warpui_core/src/text_layout.rs index 3147ed50239..49fda951c0a 100644 --- a/crates/warpui_core/src/text_layout.rs +++ b/crates/warpui_core/src/text_layout.rs @@ -269,7 +269,7 @@ impl LayoutCache { /// Removes a leading UTF-8 BOM from the text and adjusts the style run offsets accordingly. /// We throw away the styling of the BOM character. -fn strip_leading_unicode_bom<'a>( +pub(crate) fn strip_leading_unicode_bom<'a>( text: &'a str, style_runs: &'a [(Range, StyleAndFont)], ) -> (&'a str, Option>) {