From 314f3b52b4b73efa04326bd4e15cda3ec7e9ec6a Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Wed, 15 Jul 2026 16:47:03 -0700 Subject: [PATCH] =?UTF-8?q?ui-polish=20group=204:=20page=20roots=20render?= =?UTF-8?q?=20as=2022px=20headings=20with=20children=20one=20visual=20leve?= =?UTF-8?q?l=20shallower=20(render-only=20heading=5Flayout=20flag=20?= =?UTF-8?q?=E2=80=94=20tree=20depths=20and=20dump=20semantics=20untouched)?= =?UTF-8?q?;=20fix=20latent=20BlockEditor=20measure=20bug=20where=20the=20?= =?UTF-8?q?Taffy=20closure=20read=20text=5Fstyle=20outside=20the=20ancesto?= =?UTF-8?q?r=20scope,=20shrinking=20styled=20editors=20~8px=20vs=20their?= =?UTF-8?q?=20painted=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/trawler/src/editor.rs | 20 ++++++-- crates/trawler/src/main.rs | 77 ++++++++++++++++++++++++++++- openspec/changes/ui-polish/tasks.md | 6 +-- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/crates/trawler/src/editor.rs b/crates/trawler/src/editor.rs index d173920..5be0700 100644 --- a/crates/trawler/src/editor.rs +++ b/crates/trawler/src/editor.rs @@ -1039,6 +1039,18 @@ impl Element for BlockTextElement { let mut style = Style::default(); style.size.width = relative(1.).into(); let editor = self.editor.clone(); + // Capture the text style HERE, inside the ancestor style scope — + // the measure closure below runs later, during Taffy's layout + // compute, where `window.text_style()` no longer reflects this + // element's ancestors. Reading it lazily inside the closure made + // the editor *measure* at the window's base style while *painting* + // (prepaint runs back inside the scope) at the cascaded one — an + // editor inside a styled wrapper (e.g. a 22px page heading) came + // out ~8px shorter than its own painted text and shifted the rows + // below it on focus. + let text_style = window.text_style(); + let captured_font_size = text_style.font_size.to_pixels(window.rem_size()); + let captured_line_height = window.line_height(); // Wrapped height depends on the width this element ends up with, // which isn't known yet at this point in layout — `Taffy` (via // `request_measured_layout`) calls this closure back once it has @@ -1057,13 +1069,13 @@ impl Element for BlockTextElement { AvailableSpace::MinContent | AvailableSpace::MaxContent => px(400.0), }); let editor = editor.read(cx); - let style = window.text_style(); - let font_size = style.font_size.to_pixels(window.rem_size()); - let line_height = window.line_height(); + let style = &text_style; + let font_size = captured_font_size; + let line_height = captured_line_height; let (text, runs) = full_text_and_runs( &editor.content, editor.highlight_scheme, - editor_font(&style, editor.highlight_scheme), + editor_font(style, editor.highlight_scheme), style.color, ); let lines = window diff --git a/crates/trawler/src/main.rs b/crates/trawler/src/main.rs index d0aede4..92beb32 100644 --- a/crates/trawler/src/main.rs +++ b/crates/trawler/src/main.rs @@ -1883,6 +1883,13 @@ const OUTLINE_LEFT_PAD: f32 = 8.0; /// line box's geometric center. Tuned by eye; zero at the current line /// height. const BULLET_OPTICAL_NUDGE: f32 = 0.0; +/// Page-root heading text size (openspec change ui-polish, design D6): a +/// page's title renders as a large heading, not a top-level bullet, when +/// the view's roots are pages. +const PAGE_HEADING_TEXT_SIZE: f32 = 22.0; +/// Vertical padding above/below a page-root heading row. +const PAGE_HEADING_PAD_TOP: f32 = 10.0; +const PAGE_HEADING_PAD_BOTTOM: f32 = 2.0; /// A query block's own row gets this background tint — without it a query /// block was indistinguishable from an ordinary one. Deliberately applied /// only to the row, not the result section below it too: tinting both @@ -2544,6 +2551,19 @@ impl Render for TrawlerApp { let row_count = self.rows.len(); let focused_block = self.editor.as_ref().map(|e| e.block); let editor_input = self.editor.as_ref().map(|e| e.input.clone()); + // Whether this view's depth-0 rows are page roots (openspec change + // ui-polish, design D6): the journal stacks whole pages, and a + // `Node` view of a tree root is a page view. Those roots render as + // headings with their children pulled one indent level shallower — + // a rendering decision only; `VisibleRow`/dump depths stay tree + // depths. A zoomed (non-root) block keeps the ordinary bullet + // treatment. + let heading_layout = match &self.view { + View::Journal => true, + View::Node(id) => id + .as_tree_id() + .is_some_and(|t| Outline::new(self.storage.doc()).parent(t).is_none()), + }; let rows: Vec = self .rows .iter() @@ -3069,6 +3089,59 @@ impl Render for TrawlerApp { { this.schedule_query_eval(id, Duration::ZERO, cx); } + // Page-root heading (openspec change ui-polish, + // design D6): no bullet, no fold affordance, + // heading-scale text. Still the same editable + // root block — the editor variant renders here + // too, so renames happen in place, and Up from + // the first child lands on this row exactly as + // it landed on the old root bullet. A page + // toggled into a query block falls through to + // the ordinary row so its source/result UI + // stays intact. + if heading_layout && *depth == 0 && !is_query { + let heading = div() + .id(ix) + .w_full() + .pl(px(OUTLINE_LEFT_PAD)) + .pr_2() + .pt(px(PAGE_HEADING_PAD_TOP)) + .pb(px(PAGE_HEADING_PAD_BOTTOM)) + .text_size(px(PAGE_HEADING_TEXT_SIZE)) + .font_weight(FontWeight::BOLD); + return match content { + RowContent::Editor(input) => heading + .child(div().min_h(line_height).child(input.clone())), + RowContent::Markdown(blocks) => heading + .child( + div() + .id(("content", ix)) + .min_w_0() + .min_h(line_height) + .cursor_pointer() + .on_click(cx.listener( + move |this, _event: &ClickEvent, window, cx| { + this.focus_block(id, window, cx); + }, + )) + .children(render_blocks(blocks, ix, cx)), + ), + // Unreachable while `!is_query` gates + // this branch; kept total for safety. + RowContent::Source(source) => { + heading.child(render_scheme_source(source)) + } + } + .into_any_element(); + } + // Descendants of heading roots render one + // level shallower — the heading replaced the + // root bullet's level (design D6). + let indent_depth = if heading_layout { + depth.saturating_sub(1) + } else { + *depth + }; // `items_start` (not `items_center`) so the // fold arrow/bullet line up with the first line // of the content instead of the vertical center @@ -3078,7 +3151,7 @@ impl Render for TrawlerApp { let mut row = div() .id(ix) .w_full() - .pl(px(INDENT_PER_LEVEL * *depth as f32 + OUTLINE_LEFT_PAD)) + .pl(px(INDENT_PER_LEVEL * indent_depth as f32 + OUTLINE_LEFT_PAD)) .pr_2() .py_1() .flex() @@ -3223,7 +3296,7 @@ impl Render for TrawlerApp { // the indented region itself. // div() - .ml(px(INDENT_PER_LEVEL * (*depth as f32 + 1.0) + .ml(px(INDENT_PER_LEVEL * (indent_depth as f32 + 1.0) + OUTLINE_LEFT_PAD)) .mr_2() .mb_2() diff --git a/openspec/changes/ui-polish/tasks.md b/openspec/changes/ui-polish/tasks.md index c111cec..98c4fa6 100644 --- a/openspec/changes/ui-polish/tasks.md +++ b/openspec/changes/ui-polish/tasks.md @@ -19,9 +19,9 @@ ## 4. Page title as heading (design D6) -- [ ] 4.1 Render page roots as heading rows (no bullet/fold affordance; heading-scale named knobs); children start at indent level 0 -- [ ] 4.2 Preserve focus/edit/rename behavior on the heading row (Up from first child lands on it; editing = root-block editing) -- [ ] 4.3 Update UI-test layout assertions for the shallower indent; confirm devtools dump depth semantics unchanged; dev-loop screenshot of a journal page before/after +- [x] 4.1 Page roots render as heading rows (no bullet/fold affordance; `PAGE_HEADING_TEXT_SIZE`/`_PAD_TOP`/`_PAD_BOTTOM` knobs; 22px bold); children pulled one indent level shallower — implemented as a render-only per-view `heading_layout` flag (journal + page views; zoomed blocks keep bullets; a query-toggled page root falls through to the ordinary row so its source/result UI survives) +- [x] 4.2 Heading is the same editable root block: renames render in the heading at heading scale; quick-open navigation focuses it; row order unchanged so Up from first child lands on it. Fixed a latent BlockEditor bug this exposed: the Taffy measure closure read `window.text_style()` outside the ancestor style scope, so an editor in any styled wrapper measured at the base style while painting at the cascaded one (~8px row shrink on heading focus, children shifting up) — request_layout now captures the style inside the scope and the closure uses the captured values +- [x] 4.3 No UI-test assertion churn needed: depth stays tree depth everywhere (`VisibleRow`, dump) because the shift is render-only — verified via dump (pages 0, children 1); dev-loop screenshots of journal and page views, including focused-vs-unfocused layout stability ## 5. Bullet threading (design D4) — after the heading change, so guides are built against final indent geometry -- 2.51.2