From 91fa60898eb9a207a55da9bc8e6744f233f1345a Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Wed, 29 Jul 2026 22:35:03 +0800 Subject: [PATCH] fix(editor): center top bar title group --- crates/op-editor-ui/src/widgets/top_bar.rs | 50 +++++++++- .../src/widgets/top_bar_geometry.rs | 33 +++++++ .../op-editor-ui/src/widgets/top_bar_paint.rs | 12 ++- .../op-editor-ui/src/widgets/top_bar_tests.rs | 98 +++++++++++++++++-- .../op-editor-ui/src/widgets/top_bar_title.rs | 34 +++---- crates/op-host-native/src/widget_host.rs | 6 +- .../src/widget_host/cursor_move_chrome.rs | 2 +- .../widget_host/git_panel_placement_tests.rs | 5 +- .../src/widget_host/host_lifecycle.rs | 2 +- .../widget_host/image_panel_overlay_tests.rs | 2 +- .../src/widget_host/overlay_rects.rs | 2 +- .../op-host-native/src/widget_host/paint.rs | 2 +- .../src/widget_host/press_chrome_tiers.rs | 2 +- .../src/widget_host/text_edit_press.rs | 76 +++++++++----- 14 files changed, 256 insertions(+), 70 deletions(-) diff --git a/crates/op-editor-ui/src/widgets/top_bar.rs b/crates/op-editor-ui/src/widgets/top_bar.rs index 6efa68f15..ae5b80834 100644 --- a/crates/op-editor-ui/src/widgets/top_bar.rs +++ b/crates/op-editor-ui/src/widgets/top_bar.rs @@ -354,6 +354,45 @@ impl TopBar { /// - Sun (third from right) → ToggleTheme /// - Globe (fourth from right) → ToggleLocale pub fn hit_test(&self, rect: Rect, point: Point2D) -> Option { + if !rect.contains(point) { + return None; + } + let git_rect = if GIT_BUTTON_AVAILABLE && self.file_controls_visible() { + self.git_button_rect(rect) + } else { + Rect::xywh(rect.origin.x, rect.origin.y, 0.0, 0.0) + }; + self.hit_test_with_git_rect(rect, point, git_rect) + } + + /// Hit-test using the same font-family measurement as paint. + /// + /// Native hosts use this path for the centered title group's Git button so + /// its hover/press target follows the painted group even when the system + /// font's advances differ from the cross-platform fallback estimator. + pub fn hit_test_with_measure( + &self, + rect: Rect, + point: Point2D, + measure: impl FnMut(&str, f32) -> f32, + ) -> Option { + if !rect.contains(point) { + return None; + } + let git_rect = if GIT_BUTTON_AVAILABLE && self.file_controls_visible() { + self.git_button_rect_with_measure(rect, measure) + } else { + Rect::xywh(rect.origin.x, rect.origin.y, 0.0, 0.0) + }; + self.hit_test_with_git_rect(rect, point, git_rect) + } + + fn hit_test_with_git_rect( + &self, + rect: Rect, + point: Point2D, + git_rect: Rect, + ) -> Option { if !(rect).contains(point) { return None; } @@ -388,11 +427,12 @@ impl TopBar { // only — see `GIT_BUTTON_AVAILABLE`; also hidden inside a VS Code // embed alongside the rest of the file-scoped chrome — the file // name it hangs off doesn't paint either). - if GIT_BUTTON_AVAILABLE && self.file_controls_visible() { - let git_rect = self.git_button_rect(rect); - if git_rect.size.x > 0.0 && git_rect.contains(point) { - return Some(TopBarHit::ToggleGitPanel); - } + if GIT_BUTTON_AVAILABLE + && self.file_controls_visible() + && git_rect.size.x > 0.0 + && git_rect.contains(point) + { + return Some(TopBarHit::ToggleGitPanel); } // Right cluster: Maximize / Play / Sun / Globe-with-chevron // (right→left). Maximize + Play + Sun are normal ICON_BUTTON diff --git a/crates/op-editor-ui/src/widgets/top_bar_geometry.rs b/crates/op-editor-ui/src/widgets/top_bar_geometry.rs index e64b13090..92a4f32a7 100644 --- a/crates/op-editor-ui/src/widgets/top_bar_geometry.rs +++ b/crates/op-editor-ui/src/widgets/top_bar_geometry.rs @@ -259,6 +259,25 @@ impl TopBar { }) } + /// Git-button geometry using caller-provided family-aware text metrics. + /// + /// Paint and native input both use the system font. Keeping this measured + /// variant alongside the deterministic fallback prevents the centered + /// title group's variable width from shifting the visible hover target away + /// from the button. + pub fn git_button_rect_with_measure( + &self, + top_bar_rect: Rect, + measure: impl FnMut(&str, f32) -> f32, + ) -> Rect { + self.title_layout(top_bar_rect, measure) + .git_rect + .unwrap_or(Rect { + origin: Point2D::new(top_bar_rect.origin.x, top_bar_rect.origin.y), + size: Point2D::new(0.0, 0.0), + }) + } + pub(super) fn git_icon_left(git_button: Rect) -> f32 { git_button.origin.x + GIT_BUTTON_PAD_X } @@ -275,6 +294,20 @@ impl TopBar { (r.size.x > 0.0).then_some(r.origin.x + r.size.x / 2.0) } + /// Measured counterpart of [`Self::git_button_center_x`] for native + /// popover placement. + pub fn git_button_center_x_with_measure( + &self, + top_bar_rect: Rect, + measure: impl FnMut(&str, f32) -> f32, + ) -> Option { + if !GIT_BUTTON_AVAILABLE || !self.file_controls_visible() { + return None; + } + let r = self.git_button_rect_with_measure(top_bar_rect, measure); + (r.size.x > 0.0).then_some(r.origin.x + r.size.x / 2.0) + } + /// Deterministic title geometry shared by paint, hit-test, and popup /// anchoring. The estimate is intentionally conservative; paint clips to /// the returned slot as a final guard against platform font differences. diff --git a/crates/op-editor-ui/src/widgets/top_bar_paint.rs b/crates/op-editor-ui/src/widgets/top_bar_paint.rs index 4522d6deb..ba0ac53a0 100644 --- a/crates/op-editor-ui/src/widgets/top_bar_paint.rs +++ b/crates/op-editor-ui/src/widgets/top_bar_paint.rs @@ -164,11 +164,13 @@ impl TopBar { ); // ── Bounded centered file title ───────────────────── - // File name, dirty marker, and Git button share one slot between - // the import control and agent chip. Paint uses the same family - // metrics as the text runs, so the title ends exactly at the - // reserved Git gap instead of inheriting a conservative estimate. - // Clipping remains a hard guard against platform-font differences. + // File name, dirty marker, and Git button form one visual group + // centered against the full window. The slot between the import + // control and agent chip only clamps that group when the viewport + // is too narrow. Paint uses the same family metrics as the text + // runs, so the title ends exactly at the reserved Git gap instead + // of inheriting a conservative estimate. Clipping remains a hard + // guard against platform-font differences. let title = self.title_layout(rect, |text, size| { cx.backend.measure_text_family(text, size, "system-ui") }); diff --git a/crates/op-editor-ui/src/widgets/top_bar_tests.rs b/crates/op-editor-ui/src/widgets/top_bar_tests.rs index 4b3fd2e01..9c94aa549 100644 --- a/crates/op-editor-ui/src/widgets/top_bar_tests.rs +++ b/crates/op-editor-ui/src/widgets/top_bar_tests.rs @@ -1,5 +1,5 @@ use super::top_bar::*; -use super::top_bar_title::elide_filename_to_width; +use super::top_bar_title::{elide_filename_to_width, TopBarTitleLayout}; use crate::theme::Theme; use crate::widgets::icons::Icon; use crate::widgets::{PaintCx, Widget}; @@ -66,8 +66,21 @@ fn long_dirty_title_stays_between_left_and_right_controls() { ); } +fn title_group_center_x(bar: &TopBar, layout: &TopBarTitleLayout) -> f32 { + let group_right = layout + .git_rect + .map(|git| git.origin.x + git.size.x) + .or_else(|| { + layout + .edited_x + .map(|edited_x| edited_x + title_test_width(bar.label_edited, 11.0)) + }) + .unwrap_or_else(|| layout.file_x + title_test_width(&layout.file_name, 13.0)); + (layout.file_x + group_right) / 2.0 +} + #[test] -fn git_position_does_not_follow_file_name_length() { +fn title_group_stays_at_window_center_for_short_and_long_names() { let make_bar = |file_name: &str| { let mut bar = TopBar::new(file_name); bar.edited = true; @@ -79,18 +92,85 @@ fn git_position_does_not_follow_file_name_length() { }; let short = make_bar("test.op"); let long = make_bar("openpencil-super-long-project-file-name-for-title-overflow-check (1).op"); + let rect = Rect { + origin: Point2D::new(37.0, 0.0), + size: Point2D::new(1_400.0, TOP_BAR_HEIGHT), + }; + let short_layout = short.title_layout(rect, title_test_width); + let long_layout = long.title_layout(rect, title_test_width); + let window_center = rect.origin.x + rect.size.x / 2.0; + + assert!(short_layout.git_rect.is_some()); + assert!(long_layout.git_rect.is_some()); + assert!(nearly_eq( + title_group_center_x(&short, &short_layout), + window_center + )); + assert!(nearly_eq( + title_group_center_x(&long, &long_layout), + window_center + )); + assert!(long_layout.file_name.contains('…'), "{long_layout:?}"); + assert!(long_layout.file_name.ends_with(".op")); +} + +#[test] +fn title_group_center_is_independent_of_asymmetric_chrome() { + let rect = Rect { + origin: Point2D::ZERO, + size: Point2D::new(1_600.0, TOP_BAR_HEIGHT), + }; + let mut compact = TopBar::new("Untitled"); + compact.git_branch = Some("main".to_string()); + + let mut crowded = TopBar::new("Untitled"); + crowded.git_branch = Some("main".to_string()); + crowded.agent_count = 6; + crowded.connected = [true; 6]; + crowded.mcp_count = 8; + crowded.account_button_visible = true; + crowded.collab.visible = true; + crowded.collab.label = "Connected".to_string(); + + for bar in [&compact, &crowded] { + let layout = bar.title_layout(rect, title_test_width); + assert!(layout.git_rect.is_some(), "{layout:?}"); + assert!(nearly_eq( + title_group_center_x(bar, &layout), + rect.size.x / 2.0 + )); + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn measured_git_hit_tracks_the_painted_centered_group() { + let bar = TopBar::new("centered-document.op"); let rect = Rect { origin: Point2D::ZERO, size: Point2D::new(1_200.0, TOP_BAR_HEIGHT), }; - let short_git = short.git_button_rect(rect); - let long_git = long.git_button_rect(rect); - let long_layout = long.title_layout(rect, title_test_width); + let compact_metric = |text: &str, size: f32| text.chars().count() as f32 * size * 0.2; + let measured = bar.git_button_rect_with_measure(rect, compact_metric); + let estimated = bar.git_button_rect(rect); + let point = Point2D::new( + measured.origin.x + measured.size.x / 2.0, + TOP_BAR_HEIGHT / 2.0, + ); - assert!(short_git.size.x > 0.0); - assert!(nearly_eq(short_git.origin.x, long_git.origin.x)); - assert!(long_layout.file_name.contains('…'), "{long_layout:?}"); - assert!(long_layout.file_name.ends_with(".op")); + assert!( + (measured.origin.x - estimated.origin.x).abs() > ICON_BUTTON, + "fixture must distinguish exact and fallback geometry" + ); + assert_eq!( + bar.hit_test_with_measure(rect, point, compact_metric), + Some(TopBarHit::ToggleGitPanel) + ); + assert_ne!( + bar.hit_test(rect, point), + Some(TopBarHit::ToggleGitPanel), + "fallback geometry intentionally sits elsewhere in this fixture" + ); } #[test] diff --git a/crates/op-editor-ui/src/widgets/top_bar_title.rs b/crates/op-editor-ui/src/widgets/top_bar_title.rs index 5a3c508b7..f48fadafb 100644 --- a/crates/op-editor-ui/src/widgets/top_bar_title.rs +++ b/crates/op-editor-ui/src/widgets/top_bar_title.rs @@ -1,8 +1,10 @@ //! Bounded file-title layout for the `TopBar`. //! //! The title lives in the center slot between the import button and the -//! agent chip. Long file names are middle-elided while the edited marker and -//! Git button keep their own space. +//! agent chip. The visible file-name / edited-marker / Git-button group is +//! centered against the full window whenever the slot has room; left/right +//! chrome only clamps it in genuinely narrow layouts. Long file names are +//! middle-elided while the edited marker and Git button keep their own space. use crate::widgets::top_bar::*; use crate::{Point2D, Rect}; @@ -24,10 +26,11 @@ pub(super) struct TopBarTitleLayout { impl TopBar { /// Compute the title layout using one caller-supplied text metric. /// - /// Paint supplies exact backend metrics while hit-test and popup anchoring - /// use a conservative fallback. The Git anchor itself is fixed, and the - /// edited marker is reserved before the file name is elided, so it cannot - /// be pushed underneath the agent chip by a long path basename. + /// Paint supplies exact backend metrics; native hit-test and popup + /// anchoring pass the same family-aware metric, while backend-less callers + /// use a conservative fallback. The edited marker and Git button are + /// reserved before the file name is elided, so neither can be pushed under + /// the agent chip by a long path basename. pub(super) fn title_layout( &self, top_bar_rect: Rect, @@ -78,12 +81,8 @@ impl TopBar { } else { 0.0 }; - let title_box_w = (slot_width - git_span).clamp(0.0, TITLE_TEXT_MAX_WIDTH); - let desired_box_left = top_bar_rect.origin.x + (top_bar_rect.size.x - title_box_w) / 2.0; - let max_box_left = (slot_right - title_box_w - git_span).max(slot_left); - let title_box_left = desired_box_left.clamp(slot_left, max_box_left); - - let max_file_w = (title_box_w - edited_span).max(0.0); + let max_title_w = (slot_width - git_span).clamp(0.0, TITLE_TEXT_MAX_WIDTH); + let max_file_w = (max_title_w - edited_span).max(0.0); let file_name = elide_filename_to_width(&self.file_name, max_file_w, |candidate| { measure(candidate, 13.0) }); @@ -94,14 +93,15 @@ impl TopBar { 0.0 }; let title_w = file_w + actual_edited_gap + edited_w; - // Git anchors at the fixed box's right edge. Right-align the measured - // title/status content to that edge so the visible gap stays exactly - // `GIT_GAP`, independent of how much the name was elided. - let file_x = title_box_left + (title_box_w - title_w).max(0.0); + let group_w = title_w + git_span; + let desired_group_left = top_bar_rect.origin.x + (top_bar_rect.size.x - group_w) / 2.0; + let max_group_left = (slot_right - group_w).max(slot_left); + let group_left = desired_group_left.clamp(slot_left, max_group_left); + let file_x = group_left; let edited_x = self.edited.then_some(file_x + file_w + actual_edited_gap); let git_rect = show_git.then_some(Rect { origin: Point2D::new( - title_box_left + title_box_w + GIT_GAP, + group_left + title_w + GIT_GAP, top_bar_rect.origin.y + (top_bar_rect.size.y - ICON_BUTTON) / 2.0, ), size: Point2D::new(measured_git_w, ICON_BUTTON), diff --git a/crates/op-host-native/src/widget_host.rs b/crates/op-host-native/src/widget_host.rs index e118d23d8..5832a5bca 100644 --- a/crates/op-host-native/src/widget_host.rs +++ b/crates/op-host-native/src/widget_host.rs @@ -341,8 +341,10 @@ pub struct WidgetHostNative { /// Lazily-created measure-only Skia backend for text-edit /// hit-testing OUTSIDE the paint pass (press / drag / arrow-key /// line mapping). Same `measure_text_weighted` implementation the - /// paint backend uses, so hit geometry matches painted glyphs. - pub(in crate::widget_host) text_measure: Option, + /// paint backend uses, so hit geometry matches painted glyphs. Interior + /// mutability also lets read-only TopBar geometry (hover + Git popover + /// placement) use the same metrics without widening those APIs to `&mut`. + pub(in crate::widget_host) text_measure: std::cell::RefCell>, /// Active panel-resize drag — set when the cursor is pressed /// within the resize gutter of LayerPanel's right edge or /// PropertyPanel's left edge. diff --git a/crates/op-host-native/src/widget_host/cursor_move_chrome.rs b/crates/op-host-native/src/widget_host/cursor_move_chrome.rs index 857971221..cbe0f7c7b 100644 --- a/crates/op-host-native/src/widget_host/cursor_move_chrome.rs +++ b/crates/op-host-native/src/widget_host/cursor_move_chrome.rs @@ -55,7 +55,7 @@ impl WidgetHostNative { let mut top_bar = TopBar::for_editor_ui(&self.editor_state.editor_ui); top_bar.chip_text_w = Some(self.topbar_chip_text_w(&top_bar)); let new_hover = (!chat_surface_owns_point) - .then(|| top_bar.hit_test(tb_rect, Point2D::new(x, y))) + .then(|| self.topbar_hit_test(&top_bar, tb_rect, Point2D::new(x, y))) .flatten() .map(op_editor_ui::widgets::editor_state_ext::topbar_button_hover); if new_hover != self.editor_state.editor_ui.topbar_button_hover { diff --git a/crates/op-host-native/src/widget_host/git_panel_placement_tests.rs b/crates/op-host-native/src/widget_host/git_panel_placement_tests.rs index 8d2b3b83a..a1c4d6a63 100644 --- a/crates/op-host-native/src/widget_host/git_panel_placement_tests.rs +++ b/crates/op-host-native/src/widget_host/git_panel_placement_tests.rs @@ -248,8 +248,9 @@ fn git_panel_hangs_centred_under_the_git_button() { origin: Point2D::new(0.0, 0.0), size: Point2D::new(vw, TOP_BAR_HEIGHT), }; - let btn_cx = TopBar::for_editor_ui(&host.editor_state().editor_ui) - .git_button_center_x(top_bar_rect) + let top_bar = TopBar::for_editor_ui(&host.editor_state().editor_ui); + let btn_cx = host + .topbar_git_button_center_x(&top_bar, top_bar_rect) .expect("Git button is shown on desktop"); let panel_cx = r.origin.x + r.size.x / 2.0; assert!( diff --git a/crates/op-host-native/src/widget_host/host_lifecycle.rs b/crates/op-host-native/src/widget_host/host_lifecycle.rs index 8e6626f34..8324c5859 100644 --- a/crates/op-host-native/src/widget_host/host_lifecycle.rs +++ b/crates/op-host-native/src/widget_host/host_lifecycle.rs @@ -49,7 +49,7 @@ impl WidgetHostNative { image_input_geometry: None, chat_text_selection_drag: None, text_edit_selection_drag: None, - text_measure: None, + text_measure: std::cell::RefCell::new(None), panel_resize: None, variables_resize: None, node_drag: None, diff --git a/crates/op-host-native/src/widget_host/image_panel_overlay_tests.rs b/crates/op-host-native/src/widget_host/image_panel_overlay_tests.rs index b8503e54f..4d80d1e35 100644 --- a/crates/op-host-native/src/widget_host/image_panel_overlay_tests.rs +++ b/crates/op-host-native/src/widget_host/image_panel_overlay_tests.rs @@ -17,7 +17,7 @@ fn topbar_point(host: &mut WidgetHostNative, target: TopBarHit) -> Point2D { let mut x = rect.origin.x; while x < rect.origin.x + rect.size.x { let point = Point2D::new(x, TOP_BAR_HEIGHT / 2.0); - if topbar.hit_test(rect, point) == Some(target) { + if host.topbar_hit_test(&topbar, rect, point) == Some(target) { return point; } x += 1.0; diff --git a/crates/op-host-native/src/widget_host/overlay_rects.rs b/crates/op-host-native/src/widget_host/overlay_rects.rs index 559336b54..2a8459aaf 100644 --- a/crates/op-host-native/src/widget_host/overlay_rects.rs +++ b/crates/op-host-native/src/widget_host/overlay_rects.rs @@ -42,7 +42,7 @@ impl WidgetHostNative { let top_bar = TopBar::for_editor_ui(&self.editor_state.editor_ui); let min_x = canvas_left + GIT_PANEL_INSET; let max_x = (canvas_left + canvas_w - pw - GIT_PANEL_INSET).max(min_x); - let origin_x = match top_bar.git_button_center_x(top_bar_rect) { + let origin_x = match self.topbar_git_button_center_x(&top_bar, top_bar_rect) { Some(cx) => (cx - pw / 2.0).clamp(min_x, max_x), None => min_x, }; diff --git a/crates/op-host-native/src/widget_host/paint.rs b/crates/op-host-native/src/widget_host/paint.rs index 22a88400c..10bdbca7a 100644 --- a/crates/op-host-native/src/widget_host/paint.rs +++ b/crates/op-host-native/src/widget_host/paint.rs @@ -451,7 +451,7 @@ impl WidgetHostNative { // Up-caret connecting the panel to the Git button — drawn // after the panel so its base covers the panel's top border // and reads as one continuous popover surface. - if let Some(btn_cx) = top_bar.git_button_center_x(top_bar_rect) { + if let Some(btn_cx) = self.topbar_git_button_center_x(&top_bar, top_bar_rect) { let half = GIT_PANEL_CARET_HALF; let top = panel_rect.origin.y; let caret_x = btn_cx.clamp( diff --git a/crates/op-host-native/src/widget_host/press_chrome_tiers.rs b/crates/op-host-native/src/widget_host/press_chrome_tiers.rs index 09a1cfb72..e36ce5a3f 100644 --- a/crates/op-host-native/src/widget_host/press_chrome_tiers.rs +++ b/crates/op-host-native/src/widget_host/press_chrome_tiers.rs @@ -109,7 +109,7 @@ impl WidgetHostNative { }; let mut top_bar = TopBar::for_editor_ui(&self.editor_state.editor_ui); top_bar.chip_text_w = Some(self.topbar_chip_text_w(&top_bar)); - if let Some(hit) = top_bar.hit_test(top_bar_rect, Point2D::new(x, y)) { + if let Some(hit) = self.topbar_hit_test(&top_bar, top_bar_rect, Point2D::new(x, y)) { self.close_image_popovers_for_higher_overlay(); // A TopBar destination replaces the collaboration Join caret. // Clear even stale focus so reopening Join cannot resurrect it. diff --git a/crates/op-host-native/src/widget_host/text_edit_press.rs b/crates/op-host-native/src/widget_host/text_edit_press.rs index c6dec3319..6056e62f3 100644 --- a/crates/op-host-native/src/widget_host/text_edit_press.rs +++ b/crates/op-host-native/src/widget_host/text_edit_press.rs @@ -47,6 +47,9 @@ impl RenderBackend for MeasureOnly<'_> { fn measure_text_weighted(&mut self, text: &str, font_size: f32, weight: u16) -> f32 { self.inner.measure_text_weighted(text, font_size, weight) } + fn measure_text_family(&mut self, text: &str, font_size: f32, family: &str) -> f32 { + self.inner.measure_text_family(text, font_size, family) + } fn text_ascent(&mut self, font_size: f32, weight: u16) -> f32 { self.inner.text_ascent(font_size, weight) } @@ -74,25 +77,59 @@ fn inverse_rotate_doc(p: Point2D, node: &SceneNode) -> Point2D { } impl WidgetHostNative { + fn with_measure_only(&self, f: impl FnOnce(&mut MeasureOnly<'_>) -> R) -> R { + let mut measure = self + .text_measure + .borrow_mut() + .take() + .unwrap_or_else(|| crate::NativeBackend::with_dpi(1.0)); + let out = f(&mut MeasureOnly { + inner: &mut measure, + }); + *self.text_measure.borrow_mut() = Some(measure); + out + } + /// Width (px) of the agent chip's text at the paint font size (11), /// measured with the shared measure-only backend so `TopBar`'s /// agent-chip hit area matches the painted chip exactly instead of a /// char-count estimate that overran into the file-name gap. pub(in crate::widget_host) fn topbar_chip_text_w( - &mut self, + &self, top_bar: &op_editor_ui::widgets::TopBar, ) -> f32 { let chip_text = top_bar.chip_text(); - let mut measure = self - .text_measure - .take() - .unwrap_or_else(|| crate::NativeBackend::with_dpi(1.0)); - let w = MeasureOnly { - inner: &mut measure, - } - .measure_text(&chip_text, 11.0); - self.text_measure = Some(measure); - w + self.with_measure_only(|backend| backend.measure_text(&chip_text, 11.0)) + } + + /// Family-aware TopBar hit-test. The centered title group's horizontal + /// position depends on its actual glyph advances, so the native target must + /// use the same system-font measurement as paint. + pub(in crate::widget_host) fn topbar_hit_test( + &self, + top_bar: &op_editor_ui::widgets::TopBar, + rect: Rect, + point: Point2D, + ) -> Option { + self.with_measure_only(|backend| { + top_bar.hit_test_with_measure(rect, point, |text, size| { + backend.measure_text_family(text, size, "system-ui") + }) + }) + } + + /// Exact center of the painted TopBar Git button, shared by the Git + /// popover body and caret placement. + pub(in crate::widget_host) fn topbar_git_button_center_x( + &self, + top_bar: &op_editor_ui::widgets::TopBar, + rect: Rect, + ) -> Option { + self.with_measure_only(|backend| { + top_bar.git_button_center_x_with_measure(rect, |text, size| { + backend.measure_text_family(text, size, "system-ui") + }) + }) } /// The edited Text node's resolved scene node, cloned out of the @@ -117,19 +154,10 @@ impl WidgetHostNative { node: &SceneNode, f: impl FnOnce(&TextEditLayout, &mut dyn RenderBackend) -> R, ) -> R { - let mut measure = self - .text_measure - .take() - .unwrap_or_else(|| crate::NativeBackend::with_dpi(1.0)); - let out = { - let mut backend = MeasureOnly { - inner: &mut measure, - }; - let layout = text_edit_layout(&mut backend, node); - f(&layout, &mut backend) - }; - self.text_measure = Some(measure); - out + self.with_measure_only(|backend| { + let layout = text_edit_layout(backend, node); + f(&layout, backend) + }) } /// Convert a screen point to the edited node's un-rotated doc