From e17906bdfab49056fd736b4073e03f9193d09e1c Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 05:51:53 +0000 Subject: [PATCH 1/8] APP-5825 Add parallel editor layout benchmark Benchmark a stable 4,096-block EditDelta layout workload so cache contention changes can be measured on the same machine.\n\nCo-Authored-By: Warp Agent --- crates/editor/Cargo.toml | 4 + crates/editor/benches/text_layout_bench.rs | 154 +++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 crates/editor/benches/text_layout_bench.rs 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..ab37ac1a26a --- /dev/null +++ b/crates/editor/benches/text_layout_bench.rs @@ -0,0 +1,154 @@ +use std::hint::black_box; +use std::sync::Arc; +use std::time::Duration; + +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +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, +}; +use warpui_core::color::ColorU; +use warpui_core::elements::{Border, Fill}; +use warpui_core::fonts::{FamilyId, Weight}; +use warpui_core::text_layout::LayoutCache; +use warpui_core::units::IntoPixels; +use warpui_core::App; + +const BLOCK_COUNT: usize = 4_096; +const BLOCK_TEXT: &str = + "fn layout_parallel_editor_block(value: usize) -> usize { value.saturating_add(1) }\n"; + +fn benchmark_styles() -> RichTextStyles { + let white = ColorU::white(); + let paragraph = ParagraphStyles { + font_family: FamilyId(0), + 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: FamilyId(0), + 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: FamilyId(0), + font_size: 13., + cell_padding: 8., + outer_border: true, + column_dividers: true, + row_dividers: true, + }, + } +} + +fn benchmark_delta() -> EditDelta { + let block = StyledBufferBlock::Text(StyledTextBlock { + block: vec![StyledBufferRun { + run: BLOCK_TEXT.to_owned(), + text_styles: TextStylesWithMetadata::default(), + block_style: BufferBlockStyle::PlainText, + }], + style: BufferBlockStyle::PlainText, + content_length: CharOffset::from(BLOCK_TEXT.chars().count()), + }); + EditDelta { + old_offset: CharOffset::from(1) + ..CharOffset::from(1 + BLOCK_COUNT * BLOCK_TEXT.chars().count()), + new_lines: Arc::new(vec![block; BLOCK_COUNT]), + ..Default::default() + } +} + +fn text_layout_benchmark(criterion: &mut Criterion) { + let delta = benchmark_delta(); + let styles = benchmark_styles(); + let layout_options = RenderLayoutOptions::default(); + let chars = BLOCK_COUNT * BLOCK_TEXT.chars().count(); + let mut criterion = std::mem::take(criterion); + + App::test((), move |app| async move { + app.read(|ctx| { + let mut group = criterion.benchmark_group("editor_text_layout"); + group.throughput(Throughput::Elements(chars as u64)); + group.bench_function("layout_delta_4096_blocks", |bench| { + bench.iter(|| { + let layout_cache = LayoutCache::new(); + let text_layout = TextLayout::new( + &layout_cache, + ctx.font_cache().text_layout_system(), + &styles, + f32::MAX, + ); + black_box(delta.layout_delta( + &text_layout, + None, + &layout_options, + None, + 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_benchmark +} +criterion_main!(benches); From 341ccfe74901b08dfdabc898e8fe592049c730ba Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 05:59:42 +0000 Subject: [PATCH 2/8] APP-5825 Bypass layout cache for editor text Route rich-text editor layout directly through TextLayoutSystem while preserving BOM stripping and fallback font requests. Remove throwaway caches from document, placeholder, and ordered-list layout paths.\n\nCo-Authored-By: Warp Agent --- crates/editor/benches/text_layout_bench.rs | 20 +-- crates/editor/src/content/edit_tests.rs | 94 ++--------- .../src/content/mermaid_diagram_tests.rs | 19 +-- crates/editor/src/render/element/empty.rs | 10 +- crates/editor/src/render/element/header.rs | 4 +- .../editor/src/render/element/ordered_list.rs | 12 +- crates/editor/src/render/element/paragraph.rs | 10 +- .../editor/src/render/element/placeholder.rs | 5 +- crates/editor/src/render/element/task_list.rs | 2 +- .../src/render/element/unordered_list.rs | 2 +- crates/editor/src/render/layout.rs | 26 +-- crates/editor/src/render/model/mod.rs | 19 +-- .../src/render/model/offset_map_tests.rs | 9 +- crates/warpui_core/src/core/app.rs | 1 + .../src/fonts/external_fallback.rs | 1 + .../src/fonts/text_layout_system.rs | 63 +++++++- .../src/fonts/text_layout_system_tests.rs | 151 ++++++++++++++++++ crates/warpui_core/src/text_layout.rs | 2 +- 18 files changed, 272 insertions(+), 178 deletions(-) create mode 100644 crates/warpui_core/src/fonts/text_layout_system_tests.rs diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs index ab37ac1a26a..7c160a9109b 100644 --- a/crates/editor/benches/text_layout_bench.rs +++ b/crates/editor/benches/text_layout_bench.rs @@ -12,12 +12,11 @@ use warp_editor::render::model::{ BrokenLinkStyle, CheckBoxStyle, HorizontalRuleStyle, InlineCodeStyle, ParagraphStyles, RenderLayoutOptions, RichTextStyles, TableStyle, }; +use warpui_core::App; use warpui_core::color::ColorU; use warpui_core::elements::{Border, Fill}; use warpui_core::fonts::{FamilyId, Weight}; -use warpui_core::text_layout::LayoutCache; use warpui_core::units::IntoPixels; -use warpui_core::App; const BLOCK_COUNT: usize = 4_096; const BLOCK_TEXT: &str = @@ -122,20 +121,9 @@ fn text_layout_benchmark(criterion: &mut Criterion) { group.throughput(Throughput::Elements(chars as u64)); group.bench_function("layout_delta_4096_blocks", |bench| { bench.iter(|| { - let layout_cache = LayoutCache::new(); - let text_layout = TextLayout::new( - &layout_cache, - ctx.font_cache().text_layout_system(), - &styles, - f32::MAX, - ); - black_box(delta.layout_delta( - &text_layout, - None, - &layout_options, - None, - ctx, - )) + let text_layout = + TextLayout::new(ctx.font_cache().text_layout_system(), &styles, f32::MAX); + black_box(delta.layout_delta(&text_layout, None, &layout_options, None, ctx)) }) }); group.finish(); 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..9e0b4859ee3 100644 --- a/crates/editor/src/render/element/empty.rs +++ b/crates/editor/src/render/element/empty.rs @@ -31,15 +31,13 @@ impl RenderableBlock for Empty { fn layout( &mut self, model: &RenderState, - ctx: &mut warpui_core::LayoutContext, + _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, - } + .layout(&self.viewport_item, model, app, |_| placeholder::Options { + text: paragraph_placeholder_text(model.selections().len() == 1), + block_style: BufferBlockStyle::PlainText, }); } diff --git a/crates/editor/src/render/element/header.rs b/crates/editor/src/render/element/header.rs index 44a0fa6a179..7c2d160c24c 100644 --- a/crates/editor/src/render/element/header.rs +++ b/crates/editor/src/render/element/header.rs @@ -27,11 +27,11 @@ impl RenderableBlock for RenderableHeader { fn layout( &mut self, model: &RenderState, - ctx: &mut warpui_core::LayoutContext, + _ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { + .layout(&self.viewport_item, model, app, |block| { let header_size = match block { BlockItem::Header { header_size, .. } => *header_size, other => { diff --git a/crates/editor/src/render/element/ordered_list.rs b/crates/editor/src/render/element/ordered_list.rs index 9a14c5a04a4..0374e8ec04a 100644 --- a/crates/editor/src/render/element/ordered_list.rs +++ b/crates/editor/src/render/element/ordered_list.rs @@ -40,10 +40,10 @@ impl RenderableBlock for RenderableOrderedListItem { fn layout( &mut self, model: &RenderState, - ctx: &mut warpui_core::LayoutContext, + _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, @@ -63,11 +63,9 @@ impl RenderableBlock for RenderableOrderedListItem { )); self.placeholder - .layout(&self.viewport_item, model, ctx, app, |_| { - placeholder::Options { - block_style, - text: "List", - } + .layout(&self.viewport_item, model, app, |_| placeholder::Options { + block_style, + text: "List", }); } diff --git a/crates/editor/src/render/element/paragraph.rs b/crates/editor/src/render/element/paragraph.rs index 5f854655a38..069f9d63c05 100644 --- a/crates/editor/src/render/element/paragraph.rs +++ b/crates/editor/src/render/element/paragraph.rs @@ -43,15 +43,13 @@ impl RenderableBlock for RenderableParagraph { fn layout( &mut self, model: &RenderState, - ctx: &mut warpui_core::LayoutContext, + _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, - } + .layout(&self.viewport_item, model, app, |_| placeholder::Options { + text: paragraph_placeholder_text(model.selections().len() == 1), + block_style: BufferBlockStyle::PlainText, }); } diff --git a/crates/editor/src/render/element/placeholder.rs b/crates/editor/src/render/element/placeholder.rs index 1112d00e105..d252a77c148 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 super::{CursorData, RenderContext}; use crate::content::text::BufferBlockStyle; @@ -42,7 +42,6 @@ impl BlockPlaceholder { &mut self, item: &ViewportItem, model: &RenderState, - ctx: &mut LayoutContext, app: &AppContext, options: F, ) where @@ -77,7 +76,7 @@ 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), diff --git a/crates/editor/src/render/element/task_list.rs b/crates/editor/src/render/element/task_list.rs index 558e9cc0ece..55e07a4dedb 100644 --- a/crates/editor/src/render/element/task_list.rs +++ b/crates/editor/src/render/element/task_list.rs @@ -115,7 +115,7 @@ impl RenderableBlock for RenderableTaskList { app, ); self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { + .layout(&self.viewport_item, model, app, |block| { placeholder::Options { text: "To-do list", block_style: match block { diff --git a/crates/editor/src/render/element/unordered_list.rs b/crates/editor/src/render/element/unordered_list.rs index f3314cb98fd..869f9852258 100644 --- a/crates/editor/src/render/element/unordered_list.rs +++ b/crates/editor/src/render/element/unordered_list.rs @@ -70,7 +70,7 @@ impl RenderableBlock for RenderableBulletList { app, ); self.placeholder - .layout(&self.viewport_item, model, ctx, app, |block| { + .layout(&self.viewport_item, model, app, |block| { let indent_level = match block { BlockItem::UnorderedList { indent_level, .. } => *indent_level, _ => ListIndentLevel::One, diff --git a/crates/editor/src/render/layout.rs b/crates/editor/src/render/layout.rs index e892081f7d5..0d0cb05f1db 100644 --- a/crates/editor/src/render/layout.rs +++ b/crates/editor/src/render/layout.rs @@ -5,15 +5,15 @@ 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)] use warpui_core::fonts::{Style, Weight}; use warpui_core::text_layout::{ - ClipConfig, LayoutCache, Line, StyleAndFont, TextAlignment, TextBorder, TextFrame, TextStyle, + ClipConfig, 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,8 +162,7 @@ impl<'a> TextLayout<'a> { f32::MAX, alignment, None, - &self.font_cache, - ) + )) } /// Lays out placeholder text for empty blocks. @@ -188,14 +179,13 @@ 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( + Arc::new(self.font_cache.layout_line_uncached( text, paragraph_styles.line_style(), style_runs, self.content_width(spacing), ClipConfig::end(), - &self.font_cache, - ) + )) } /// Returns the maximum width for text content laid out with the given spacing. 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..a4374ca9b3d 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]. @@ -36,6 +38,24 @@ impl TextLayoutSystem<'_> { .layout_line(text, line_style, style_runs, max_width, clip_config) } + /// Lays out a line without retaining it in a [`crate::text_layout::LayoutCache`]. + pub fn layout_line_uncached( + &self, + text: &str, + line_style: LineStyle, + style_runs: &[(Range, StyleAndFont)], + max_width: f32, + clip_config: ClipConfig, + ) -> Line { + 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 line = self.layout_line(text, line_style, style_runs, max_width, clip_config); + self.request_fallback_fonts(&line.chars_with_missing_glyphs); + line + } + #[allow(clippy::too_many_arguments)] pub fn layout_text( &self, @@ -57,4 +77,45 @@ 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); + } + } } + +#[cfg(test)] +#[path = "text_layout_system_tests.rs"] +mod tests; diff --git a/crates/warpui_core/src/fonts/text_layout_system_tests.rs b/crates/warpui_core/src/fonts/text_layout_system_tests.rs new file mode 100644 index 00000000000..0335cf7a5c6 --- /dev/null +++ b/crates/warpui_core/src/fonts/text_layout_system_tests.rs @@ -0,0 +1,151 @@ +use std::ops::Range; +use std::sync::{Arc, Mutex}; + +use super::*; +use crate::fonts::{ExternalFontFamily, FamilyId, FontFallbackCache, Properties}; +use crate::text_layout::{StyleAndFont, TextStyle}; + +#[derive(Default)] +struct RecordingLayoutSystem { + line_input: Mutex>)>>, + text_input: Mutex>)>>, + missing_glyph: Option, +} + +impl platform::TextLayoutSystem for RecordingLayoutSystem { + fn layout_line( + &self, + text: &str, + line_style: LineStyle, + style_runs: &[(Range, StyleAndFont)], + _max_width: f32, + _clip_config: ClipConfig, + ) -> Line { + *self.line_input.lock().unwrap() = Some(( + text.to_owned(), + style_runs.iter().map(|(range, _)| range.clone()).collect(), + )); + let mut line = Line::empty(line_style.font_size, line_style.line_height_ratio, 0); + line.chars_with_missing_glyphs.extend(self.missing_glyph); + line + } + + fn layout_text( + &self, + text: &str, + line_style: LineStyle, + style_runs: &[(Range, StyleAndFont)], + _max_width: f32, + _max_height: f32, + _alignment: TextAlignment, + _first_line_head_indent: Option, + ) -> TextFrame { + *self.text_input.lock().unwrap() = Some(( + text.to_owned(), + style_runs.iter().map(|(range, _)| range.clone()).collect(), + )); + TextFrame::empty(line_style.font_size, line_style.line_height_ratio) + } +} + +fn style() -> StyleAndFont { + StyleAndFont::new(FamilyId(0), Properties::default(), TextStyle::default()) +} + +fn line_style() -> LineStyle { + LineStyle { + font_size: 13., + line_height_ratio: 1.2, + baseline_ratio: 0.8, + fixed_width_tab_size: None, + } +} + +#[test] +fn uncached_line_layout_strips_a_leading_bom_and_adjusts_styles() { + let platform = RecordingLayoutSystem::default(); + let cache = FontFallbackCache::default(); + let system = TextLayoutSystem { + platform: &platform, + cache: &cache, + }; + + system.layout_line_uncached( + "\u{feff}hello", + line_style(), + &[(0..1, style()), (1..6, style())], + 100., + ClipConfig::end(), + ); + + assert_eq!( + platform.line_input.lock().unwrap().as_ref(), + Some(&("hello".to_owned(), vec![0..0, 0..5])) + ); +} + +#[test] +fn uncached_text_layout_strips_a_leading_bom_and_adjusts_styles() { + let platform = RecordingLayoutSystem::default(); + let cache = FontFallbackCache::default(); + let system = TextLayoutSystem { + platform: &platform, + cache: &cache, + }; + + system.layout_text_uncached( + "\u{feff}hello", + line_style(), + &[(0..1, style()), (1..6, style())], + 100., + 100., + TextAlignment::Left, + None, + ); + + assert_eq!( + platform.text_input.lock().unwrap().as_ref(), + Some(&("hello".to_owned(), vec![0..0, 0..5])) + ); +} + +#[test] +fn uncached_layout_requests_fallback_fonts_for_missing_glyphs() { + let missing_glyph = '🦀'; + let platform = RecordingLayoutSystem { + missing_glyph: Some(missing_glyph), + ..Default::default() + }; + let cache = FontFallbackCache { + fallback_font_fn: Some(Box::new(move |ch| { + (ch == missing_glyph).then(|| ExternalFontFamily { + font_urls: Arc::new(vec!["https://example.com/fallback.ttf".to_owned()]), + name: "Test fallback", + }) + })), + ..Default::default() + }; + let system = TextLayoutSystem { + platform: &platform, + cache: &cache, + }; + + system.layout_line_uncached( + "🦀", + line_style(), + &[(0..1, style())], + 100., + ClipConfig::end(), + ); + + let requested = cache + .requested_fallback_families + .iter() + .next() + .expect("fallback font should be requested"); + assert_eq!(requested.key().name, "Test fallback"); + assert!(matches!( + requested.value().as_slice(), + [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>) { From 43d762a089bac3b7a2bd06ac93df0d1f5671fb79 Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:24:58 +0000 Subject: [PATCH 3/8] APP-5825 Cover text-frame fallback requests Exercise missing-glyph fallback registration through layout_text_uncached, matching the document and ordered-list-number path.\n\nCo-Authored-By: Warp Agent --- .../src/fonts/text_layout_system_tests.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/warpui_core/src/fonts/text_layout_system_tests.rs b/crates/warpui_core/src/fonts/text_layout_system_tests.rs index 0335cf7a5c6..cd355ad4cbe 100644 --- a/crates/warpui_core/src/fonts/text_layout_system_tests.rs +++ b/crates/warpui_core/src/fonts/text_layout_system_tests.rs @@ -1,6 +1,8 @@ use std::ops::Range; use std::sync::{Arc, Mutex}; +use vec1::vec1; + use super::*; use crate::fonts::{ExternalFontFamily, FamilyId, FontFallbackCache, Properties}; use crate::text_layout::{StyleAndFont, TextStyle}; @@ -37,14 +39,16 @@ impl platform::TextLayoutSystem for RecordingLayoutSystem { style_runs: &[(Range, StyleAndFont)], _max_width: f32, _max_height: f32, - _alignment: TextAlignment, + alignment: TextAlignment, _first_line_head_indent: Option, ) -> TextFrame { *self.text_input.lock().unwrap() = Some(( text.to_owned(), style_runs.iter().map(|(range, _)| range.clone()).collect(), )); - TextFrame::empty(line_style.font_size, line_style.line_height_ratio) + let mut line = Line::empty(line_style.font_size, line_style.line_height_ratio, 0); + line.chars_with_missing_glyphs.extend(self.missing_glyph); + TextFrame::new(vec1![line], 0., alignment) } } @@ -110,7 +114,7 @@ fn uncached_text_layout_strips_a_leading_bom_and_adjusts_styles() { } #[test] -fn uncached_layout_requests_fallback_fonts_for_missing_glyphs() { +fn uncached_text_layout_requests_fallback_fonts_for_missing_glyphs() { let missing_glyph = '🦀'; let platform = RecordingLayoutSystem { missing_glyph: Some(missing_glyph), @@ -130,12 +134,14 @@ fn uncached_layout_requests_fallback_fonts_for_missing_glyphs() { cache: &cache, }; - system.layout_line_uncached( + system.layout_text_uncached( "🦀", line_style(), &[(0..1, style())], 100., - ClipConfig::end(), + 100., + TextAlignment::Left, + None, ); let requested = cache From a67c645d72663e59588ee5e888b6ae175a28bb2f Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:09:17 +0000 Subject: [PATCH 4/8] APP-5825 benchmark native CoreText layout --- crates/editor/benches/text_layout_bench.rs | 54 +++++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs index 7c160a9109b..9d3c13905fa 100644 --- a/crates/editor/benches/text_layout_bench.rs +++ b/crates/editor/benches/text_layout_bench.rs @@ -17,15 +17,17 @@ use warpui_core::color::ColorU; use warpui_core::elements::{Border, Fill}; use warpui_core::fonts::{FamilyId, Weight}; use warpui_core::units::IntoPixels; +#[cfg(target_os = "macos")] +use {warpui::platform::mac::FontDB as MacFontDB, warpui_core::fonts::Cache as FontCache}; const BLOCK_COUNT: usize = 4_096; const BLOCK_TEXT: &str = "fn layout_parallel_editor_block(value: usize) -> usize { value.saturating_add(1) }\n"; -fn benchmark_styles() -> RichTextStyles { +fn benchmark_styles(font_family: FamilyId) -> RichTextStyles { let white = ColorU::white(); let paragraph = ParagraphStyles { - font_family: FamilyId(0), + font_family, font_size: 13., font_weight: Weight::Normal, line_height_ratio: 1.2, @@ -47,7 +49,7 @@ fn benchmark_styles() -> RichTextStyles { selection_fill: Fill::None, cursor_fill: Fill::None, inline_code_style: InlineCodeStyle { - font_family: FamilyId(0), + font_family, background: white, font_color: white, }, @@ -108,16 +110,16 @@ fn benchmark_delta() -> EditDelta { } } -fn text_layout_benchmark(criterion: &mut Criterion) { +fn test_backend_text_layout_benchmark(criterion: &mut Criterion) { let delta = benchmark_delta(); - let styles = benchmark_styles(); + let styles = benchmark_styles(FamilyId(0)); let layout_options = RenderLayoutOptions::default(); let chars = BLOCK_COUNT * BLOCK_TEXT.chars().count(); let mut criterion = std::mem::take(criterion); App::test((), move |app| async move { app.read(|ctx| { - let mut group = criterion.benchmark_group("editor_text_layout"); + let mut group = criterion.benchmark_group("editor_text_layout/test_backend"); group.throughput(Throughput::Elements(chars as u64)); group.bench_function("layout_delta_4096_blocks", |bench| { bench.iter(|| { @@ -130,13 +132,51 @@ fn text_layout_benchmark(criterion: &mut Criterion) { }); }); } +#[cfg(target_os = "macos")] +fn core_text_layout_benchmark(criterion: &mut Criterion) { + 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 = benchmark_delta(); + let styles = benchmark_styles(font_family); + let layout_options = RenderLayoutOptions::default(); + let chars = BLOCK_COUNT * BLOCK_TEXT.chars().count(); + let mut criterion = std::mem::take(criterion); + App::test((), move |app| async move { + app.read(|ctx| { + let mut group = criterion.benchmark_group("editor_text_layout/core_text"); + group.throughput(Throughput::Elements(chars as u64)); + group.bench_function("layout_delta_4096_blocks", |bench| { + bench.iter(|| { + let text_layout = + TextLayout::new(font_cache.text_layout_system(), &styles, f32::MAX); + black_box(delta.layout_delta(&text_layout, None, &layout_options, None, ctx)) + }) + }); + group.finish(); + }); + }); +} + +#[cfg(target_os = "macos")] + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(20) + .warm_up_time(Duration::from_secs(2)) + .measurement_time(Duration::from_secs(5)); + targets = test_backend_text_layout_benchmark, core_text_layout_benchmark +} +#[cfg(not(target_os = "macos"))] 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_benchmark + targets = test_backend_text_layout_benchmark } criterion_main!(benches); From bfc5b97ad14bb65714613efccb774cae10f5b2f3 Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:11:29 +0000 Subject: [PATCH 5/8] APP-5825 benchmark unique native layout blocks --- crates/editor/benches/text_layout_bench.rs | 59 +++++++++++++--------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs index 9d3c13905fa..56a65b39e6f 100644 --- a/crates/editor/benches/text_layout_bench.rs +++ b/crates/editor/benches/text_layout_bench.rs @@ -23,6 +23,18 @@ use {warpui::platform::mac::FontDB as MacFontDB, warpui_core::fonts::Cache as Fo const BLOCK_COUNT: usize = 4_096; const BLOCK_TEXT: &str = "fn layout_parallel_editor_block(value: usize) -> usize { value.saturating_add(1) }\n"; +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(); @@ -82,7 +94,7 @@ fn benchmark_styles(font_family: FamilyId) -> RichTextStyles { header_text_color: white, scrollbar_nonactive_thumb_color: white, scrollbar_active_thumb_color: white, - font_family: FamilyId(0), + font_family, font_size: 13., cell_padding: 8., outer_border: true, @@ -92,36 +104,34 @@ fn benchmark_styles(font_family: FamilyId) -> RichTextStyles { } } -fn benchmark_delta() -> EditDelta { - let block = StyledBufferBlock::Text(StyledTextBlock { - block: vec![StyledBufferRun { - run: BLOCK_TEXT.to_owned(), - text_styles: TextStylesWithMetadata::default(), - block_style: BufferBlockStyle::PlainText, - }], - style: BufferBlockStyle::PlainText, - content_length: CharOffset::from(BLOCK_TEXT.chars().count()), - }); - EditDelta { - old_offset: CharOffset::from(1) - ..CharOffset::from(1 + BLOCK_COUNT * BLOCK_TEXT.chars().count()), - new_lines: Arc::new(vec![block; BLOCK_COUNT]), - ..Default::default() - } +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 test_backend_text_layout_benchmark(criterion: &mut Criterion) { - let delta = benchmark_delta(); + let (delta, chars) = benchmark_delta(std::iter::repeat_n(BLOCK_TEXT.to_owned(), BLOCK_COUNT)); let styles = benchmark_styles(FamilyId(0)); let layout_options = RenderLayoutOptions::default(); - let chars = BLOCK_COUNT * BLOCK_TEXT.chars().count(); let mut criterion = std::mem::take(criterion); App::test((), move |app| async move { app.read(|ctx| { let mut group = criterion.benchmark_group("editor_text_layout/test_backend"); group.throughput(Throughput::Elements(chars as u64)); - group.bench_function("layout_delta_4096_blocks", |bench| { + group.bench_function("layout_delta_4096_identical_blocks", |bench| { bench.iter(|| { let text_layout = TextLayout::new(ctx.font_cache().text_layout_system(), &styles, f32::MAX); @@ -138,17 +148,20 @@ fn core_text_layout_benchmark(criterion: &mut Criterion) { let font_family = font_cache .load_system_font("Menlo") .expect("Menlo should be available on macOS"); - let delta = benchmark_delta(); + let (delta, chars) = benchmark_delta((0..BLOCK_COUNT).map(|index| { + format!( + "fn layout_parallel_editor_block_{index}(value: usize) -> usize {{ value.saturating_add(1) }}\n" + ) + })); let styles = benchmark_styles(font_family); let layout_options = RenderLayoutOptions::default(); - let chars = BLOCK_COUNT * BLOCK_TEXT.chars().count(); let mut criterion = std::mem::take(criterion); App::test((), move |app| async move { app.read(|ctx| { let mut group = criterion.benchmark_group("editor_text_layout/core_text"); group.throughput(Throughput::Elements(chars as u64)); - group.bench_function("layout_delta_4096_blocks", |bench| { + group.bench_function("layout_delta_4096_unique_blocks", |bench| { bench.iter(|| { let text_layout = TextLayout::new(font_cache.text_layout_system(), &styles, f32::MAX); From 89349a15ff4debc42ccd4097df196517cb49b26e Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:17:56 +0000 Subject: [PATCH 6/8] APP-5825 preserve Criterion benchmark config --- crates/editor/benches/text_layout_bench.rs | 116 +++++++++++---------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs index 56a65b39e6f..61b48902949 100644 --- a/crates/editor/benches/text_layout_bench.rs +++ b/crates/editor/benches/text_layout_bench.rs @@ -121,75 +121,81 @@ fn benchmark_delta(texts: impl IntoIterator) -> (EditDelta, usize ) } -fn test_backend_text_layout_benchmark(criterion: &mut Criterion) { - let (delta, chars) = benchmark_delta(std::iter::repeat_n(BLOCK_TEXT.to_owned(), BLOCK_COUNT)); - let styles = benchmark_styles(FamilyId(0)); - let layout_options = RenderLayoutOptions::default(); - let mut criterion = std::mem::take(criterion); - - App::test((), move |app| async move { - app.read(|ctx| { - let mut group = criterion.benchmark_group("editor_text_layout/test_backend"); - group.throughput(Throughput::Elements(chars as u64)); - group.bench_function("layout_delta_4096_identical_blocks", |bench| { - bench.iter(|| { - let text_layout = - TextLayout::new(ctx.font_cache().text_layout_system(), &styles, f32::MAX); - black_box(delta.layout_delta(&text_layout, None, &layout_options, None, ctx)) - }) - }); - group.finish(); - }); - }); -} -#[cfg(target_os = "macos")] -fn core_text_layout_benchmark(criterion: &mut Criterion) { - 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!( - "fn layout_parallel_editor_block_{index}(value: usize) -> usize {{ value.saturating_add(1) }}\n" - ) - })); - let styles = benchmark_styles(font_family); +fn text_layout_benchmarks(criterion: &mut Criterion) { + let (test_delta, test_chars) = + benchmark_delta(std::iter::repeat_n(BLOCK_TEXT.to_owned(), BLOCK_COUNT)); + 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!( + "fn layout_parallel_editor_block_{index}(value: usize) -> usize {{ value.saturating_add(1) }}\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 mut group = criterion.benchmark_group("editor_text_layout/core_text"); - group.throughput(Throughput::Elements(chars as u64)); - group.bench_function("layout_delta_4096_unique_blocks", |bench| { - bench.iter(|| { - let text_layout = - TextLayout::new(font_cache.text_layout_system(), &styles, f32::MAX); - black_box(delta.layout_delta(&text_layout, None, &layout_options, None, ctx)) - }) - }); - group.finish(); + { + 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_identical_blocks", |bench| { + bench.iter(|| { + let text_layout = TextLayout::new( + ctx.font_cache().text_layout_system(), + &test_styles, + f32::MAX, + ); + black_box(test_delta.layout_delta( + &text_layout, + None, + &layout_options, + None, + 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_unique_blocks", |bench| { + bench.iter(|| { + let text_layout = TextLayout::new( + core_text_font_cache.text_layout_system(), + &core_text_styles, + f32::MAX, + ); + black_box(core_text_delta.layout_delta( + &text_layout, + None, + &layout_options, + None, + ctx, + )) + }) + }); + group.finish(); + } }); }); } -#[cfg(target_os = "macos")] - -criterion_group! { - name = benches; - config = Criterion::default() - .sample_size(20) - .warm_up_time(Duration::from_secs(2)) - .measurement_time(Duration::from_secs(5)); - targets = test_backend_text_layout_benchmark, core_text_layout_benchmark -} -#[cfg(not(target_os = "macos"))] criterion_group! { name = benches; config = Criterion::default() .sample_size(20) .warm_up_time(Duration::from_secs(2)) .measurement_time(Duration::from_secs(5)); - targets = test_backend_text_layout_benchmark + targets = text_layout_benchmarks } criterion_main!(benches); From b5a760903871146b29831e690e393aca1407b590 Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:04:39 +0000 Subject: [PATCH 7/8] fix(editor): refine uncached layout benchmark --- crates/editor/benches/text_layout_bench.rs | 80 ++++++--- crates/editor/src/render/element/empty.rs | 13 +- crates/editor/src/render/element/header.rs | 13 +- .../editor/src/render/element/ordered_list.rs | 13 +- crates/editor/src/render/element/paragraph.rs | 13 +- .../editor/src/render/element/placeholder.rs | 10 +- crates/editor/src/render/element/task_list.rs | 41 ++--- .../src/render/element/unordered_list.rs | 11 +- crates/editor/src/render/layout.rs | 8 +- .../src/fonts/text_layout_system.rs | 4 - .../src/fonts/text_layout_system_tests.rs | 157 ------------------ 11 files changed, 132 insertions(+), 231 deletions(-) delete mode 100644 crates/warpui_core/src/fonts/text_layout_system_tests.rs diff --git a/crates/editor/benches/text_layout_bench.rs b/crates/editor/benches/text_layout_bench.rs index 61b48902949..13dc0b1d0e1 100644 --- a/crates/editor/benches/text_layout_bench.rs +++ b/crates/editor/benches/text_layout_bench.rs @@ -3,6 +3,7 @@ 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; @@ -12,17 +13,27 @@ 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; -#[cfg(target_os = "macos")] -use {warpui::platform::mac::FontDB as MacFontDB, warpui_core::fonts::Cache as FontCache}; const BLOCK_COUNT: usize = 4_096; -const BLOCK_TEXT: &str = - "fn layout_parallel_editor_block(value: usize) -> usize { value.saturating_add(1) }\n"; +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 { @@ -121,9 +132,23 @@ fn benchmark_delta(texts: impl IntoIterator) -> (EditDelta, usize ) } +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) { - let (test_delta, test_chars) = - benchmark_delta(std::iter::repeat_n(BLOCK_TEXT.to_owned(), BLOCK_COUNT)); + 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")] @@ -132,34 +157,41 @@ fn text_layout_benchmarks(criterion: &mut Criterion) { 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!( - "fn layout_parallel_editor_block_{index}(value: usize) -> usize {{ value.saturating_add(1) }}\n" - ) - })); + 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_identical_blocks", |bench| { + 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, ); - black_box(test_delta.layout_delta( - &text_layout, - None, - &layout_options, - None, - ctx, - )) + layout_delta(&test_delta, &text_layout, &layout_options, ctx) }) }); group.finish(); @@ -168,20 +200,14 @@ fn text_layout_benchmarks(criterion: &mut Criterion) { { 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_unique_blocks", |bench| { + 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, ); - black_box(core_text_delta.layout_delta( - &text_layout, - None, - &layout_options, - None, - ctx, - )) + layout_delta(&core_text_delta, &text_layout, &layout_options, ctx) }) }); group.finish(); diff --git a/crates/editor/src/render/element/empty.rs b/crates/editor/src/render/element/empty.rs index 9e0b4859ee3..6c0f585c8d4 100644 --- a/crates/editor/src/render/element/empty.rs +++ b/crates/editor/src/render/element/empty.rs @@ -31,14 +31,19 @@ impl RenderableBlock for Empty { fn layout( &mut self, model: &RenderState, - _ctx: &mut warpui_core::LayoutContext, + ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, app, |_| placeholder::Options { + 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 7c2d160c24c..e1ee9c1a9d4 100644 --- a/crates/editor/src/render/element/header.rs +++ b/crates/editor/src/render/element/header.rs @@ -27,11 +27,15 @@ impl RenderableBlock for RenderableHeader { fn layout( &mut self, model: &RenderState, - _ctx: &mut warpui_core::LayoutContext, + ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, 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 0374e8ec04a..90312507bf0 100644 --- a/crates/editor/src/render/element/ordered_list.rs +++ b/crates/editor/src/render/element/ordered_list.rs @@ -40,7 +40,7 @@ impl RenderableBlock for RenderableOrderedListItem { fn layout( &mut self, model: &RenderState, - _ctx: &mut warpui_core::LayoutContext, + ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { let text_layout = TextLayout::for_render_state(app, model); @@ -62,11 +62,16 @@ impl RenderableBlock for RenderableOrderedListItem { style_runs, )); - self.placeholder - .layout(&self.viewport_item, model, app, |_| placeholder::Options { + 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 069f9d63c05..e4853857927 100644 --- a/crates/editor/src/render/element/paragraph.rs +++ b/crates/editor/src/render/element/paragraph.rs @@ -43,14 +43,19 @@ impl RenderableBlock for RenderableParagraph { fn layout( &mut self, model: &RenderState, - _ctx: &mut warpui_core::LayoutContext, + ctx: &mut warpui_core::LayoutContext, app: &warpui_core::AppContext, ) { - self.placeholder - .layout(&self.viewport_item, model, app, |_| placeholder::Options { + 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 d252a77c148..f28fa667192 100644 --- a/crates/editor/src/render/element/placeholder.rs +++ b/crates/editor/src/render/element/placeholder.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use warpui_core::AppContext; use warpui_core::geometry::vector::{Vector2F, vec2f}; -use warpui_core::text_layout::Line; +use warpui_core::text_layout::{LayoutCache, Line}; use super::{CursorData, RenderContext}; use crate::content::text::BufferBlockStyle; @@ -42,6 +42,7 @@ impl BlockPlaceholder { &mut self, item: &ViewportItem, model: &RenderState, + layout_cache: &LayoutCache, app: &AppContext, options: F, ) where @@ -79,7 +80,12 @@ impl BlockPlaceholder { 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 55e07a4dedb..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, 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 869f9852258..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, 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 0d0cb05f1db..a17ba62aa1b 100644 --- a/crates/editor/src/render/layout.rs +++ b/crates/editor/src/render/layout.rs @@ -11,7 +11,7 @@ use warpui_core::fonts::TextLayoutSystem; #[cfg(test)] use warpui_core::fonts::{Style, Weight}; use warpui_core::text_layout::{ - ClipConfig, Line, StyleAndFont, TextAlignment, TextBorder, TextFrame, TextStyle, + ClipConfig, LayoutCache, Line, StyleAndFont, TextAlignment, TextBorder, TextFrame, TextStyle, }; use warpui_core::units::{IntoPixels, Pixels}; @@ -168,6 +168,7 @@ impl<'a> TextLayout<'a> { /// Lays out placeholder text for empty blocks. pub fn layout_placeholder( &self, + layout_cache: &LayoutCache, text: &str, block_type: &BufferBlockStyle, spacing: &BlockSpacing, @@ -179,13 +180,14 @@ impl<'a> TextLayout<'a> { ); let text = truncate_text_for_layout(text); let style_runs = &[(0..text.chars().count(), style_and_font)]; - Arc::new(self.font_cache.layout_line_uncached( + layout_cache.layout_line( text, paragraph_styles.line_style(), style_runs, self.content_width(spacing), ClipConfig::end(), - )) + &self.font_cache, + ) } /// Returns the maximum width for text content laid out with the given spacing. diff --git a/crates/warpui_core/src/fonts/text_layout_system.rs b/crates/warpui_core/src/fonts/text_layout_system.rs index a4374ca9b3d..19dee290806 100644 --- a/crates/warpui_core/src/fonts/text_layout_system.rs +++ b/crates/warpui_core/src/fonts/text_layout_system.rs @@ -115,7 +115,3 @@ impl TextLayoutSystem<'_> { } } } - -#[cfg(test)] -#[path = "text_layout_system_tests.rs"] -mod tests; diff --git a/crates/warpui_core/src/fonts/text_layout_system_tests.rs b/crates/warpui_core/src/fonts/text_layout_system_tests.rs deleted file mode 100644 index cd355ad4cbe..00000000000 --- a/crates/warpui_core/src/fonts/text_layout_system_tests.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::ops::Range; -use std::sync::{Arc, Mutex}; - -use vec1::vec1; - -use super::*; -use crate::fonts::{ExternalFontFamily, FamilyId, FontFallbackCache, Properties}; -use crate::text_layout::{StyleAndFont, TextStyle}; - -#[derive(Default)] -struct RecordingLayoutSystem { - line_input: Mutex>)>>, - text_input: Mutex>)>>, - missing_glyph: Option, -} - -impl platform::TextLayoutSystem for RecordingLayoutSystem { - fn layout_line( - &self, - text: &str, - line_style: LineStyle, - style_runs: &[(Range, StyleAndFont)], - _max_width: f32, - _clip_config: ClipConfig, - ) -> Line { - *self.line_input.lock().unwrap() = Some(( - text.to_owned(), - style_runs.iter().map(|(range, _)| range.clone()).collect(), - )); - let mut line = Line::empty(line_style.font_size, line_style.line_height_ratio, 0); - line.chars_with_missing_glyphs.extend(self.missing_glyph); - line - } - - fn layout_text( - &self, - text: &str, - line_style: LineStyle, - style_runs: &[(Range, StyleAndFont)], - _max_width: f32, - _max_height: f32, - alignment: TextAlignment, - _first_line_head_indent: Option, - ) -> TextFrame { - *self.text_input.lock().unwrap() = Some(( - text.to_owned(), - style_runs.iter().map(|(range, _)| range.clone()).collect(), - )); - let mut line = Line::empty(line_style.font_size, line_style.line_height_ratio, 0); - line.chars_with_missing_glyphs.extend(self.missing_glyph); - TextFrame::new(vec1![line], 0., alignment) - } -} - -fn style() -> StyleAndFont { - StyleAndFont::new(FamilyId(0), Properties::default(), TextStyle::default()) -} - -fn line_style() -> LineStyle { - LineStyle { - font_size: 13., - line_height_ratio: 1.2, - baseline_ratio: 0.8, - fixed_width_tab_size: None, - } -} - -#[test] -fn uncached_line_layout_strips_a_leading_bom_and_adjusts_styles() { - let platform = RecordingLayoutSystem::default(); - let cache = FontFallbackCache::default(); - let system = TextLayoutSystem { - platform: &platform, - cache: &cache, - }; - - system.layout_line_uncached( - "\u{feff}hello", - line_style(), - &[(0..1, style()), (1..6, style())], - 100., - ClipConfig::end(), - ); - - assert_eq!( - platform.line_input.lock().unwrap().as_ref(), - Some(&("hello".to_owned(), vec![0..0, 0..5])) - ); -} - -#[test] -fn uncached_text_layout_strips_a_leading_bom_and_adjusts_styles() { - let platform = RecordingLayoutSystem::default(); - let cache = FontFallbackCache::default(); - let system = TextLayoutSystem { - platform: &platform, - cache: &cache, - }; - - system.layout_text_uncached( - "\u{feff}hello", - line_style(), - &[(0..1, style()), (1..6, style())], - 100., - 100., - TextAlignment::Left, - None, - ); - - assert_eq!( - platform.text_input.lock().unwrap().as_ref(), - Some(&("hello".to_owned(), vec![0..0, 0..5])) - ); -} - -#[test] -fn uncached_text_layout_requests_fallback_fonts_for_missing_glyphs() { - let missing_glyph = '🦀'; - let platform = RecordingLayoutSystem { - missing_glyph: Some(missing_glyph), - ..Default::default() - }; - let cache = FontFallbackCache { - fallback_font_fn: Some(Box::new(move |ch| { - (ch == missing_glyph).then(|| ExternalFontFamily { - font_urls: Arc::new(vec!["https://example.com/fallback.ttf".to_owned()]), - name: "Test fallback", - }) - })), - ..Default::default() - }; - let system = TextLayoutSystem { - platform: &platform, - cache: &cache, - }; - - system.layout_text_uncached( - "🦀", - line_style(), - &[(0..1, style())], - 100., - 100., - TextAlignment::Left, - None, - ); - - let requested = cache - .requested_fallback_families - .iter() - .next() - .expect("fallback font should be requested"); - assert_eq!(requested.key().name, "Test fallback"); - assert!(matches!( - requested.value().as_slice(), - [RequestedFallbackFontSource::UncachedText] - )); -} From 3af13a57e2af46a9c9745c65426b0054a43a79cc Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:21:58 +0000 Subject: [PATCH 8/8] refactor(warpui): remove unused uncached line layout --- .../src/fonts/text_layout_system.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/warpui_core/src/fonts/text_layout_system.rs b/crates/warpui_core/src/fonts/text_layout_system.rs index 19dee290806..9dd3a36aae0 100644 --- a/crates/warpui_core/src/fonts/text_layout_system.rs +++ b/crates/warpui_core/src/fonts/text_layout_system.rs @@ -38,24 +38,6 @@ impl TextLayoutSystem<'_> { .layout_line(text, line_style, style_runs, max_width, clip_config) } - /// Lays out a line without retaining it in a [`crate::text_layout::LayoutCache`]. - pub fn layout_line_uncached( - &self, - text: &str, - line_style: LineStyle, - style_runs: &[(Range, StyleAndFont)], - max_width: f32, - clip_config: ClipConfig, - ) -> Line { - 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 line = self.layout_line(text, line_style, style_runs, max_width, clip_config); - self.request_fallback_fonts(&line.chars_with_missing_glyphs); - line - } - #[allow(clippy::too_many_arguments)] pub fn layout_text( &self,