diff --git a/assets/locales/pending/browser-reveal-on-hover.json b/assets/locales/pending/browser-reveal-on-hover.json new file mode 100644 index 0000000000..08cc54b130 --- /dev/null +++ b/assets/locales/pending/browser-reveal-on-hover.json @@ -0,0 +1,47 @@ +{ + "en": { + "prefs.browser_reveal_on_hover": "Reveal on hover", + "prefs.browser_show_note": "Shown keeps the browser on screen. Reveal on hover keeps it out of sight until the pointer comes over its place or the thin tab at its left edge, and it stays open while the pointer is inside it or a menu from it is open.", + "prefs.browser_shown": "Shown" + }, + "ar": { + "prefs.browser_reveal_on_hover": "إظهار عند التمرير", + "prefs.browser_show_note": "«ظاهر» يُبقي المستعرض على الشاشة. «إظهار عند التمرير» يُخفيه حتى يمر المؤشر فوق مكانه أو فوق الشريط الرفيع عند حافته اليسرى، ويبقى مفتوحًا ما دام المؤشر داخله أو كانت قائمة منه مفتوحة.", + "prefs.browser_shown": "ظاهر" + }, + "de": { + "prefs.browser_reveal_on_hover": "Beim Überfahren einblenden", + "prefs.browser_show_note": "Angezeigt lässt den Browser auf dem Bildschirm. Beim Überfahren einblenden blendet ihn aus, bis der Zeiger über seinen Platz oder die schmale Lasche an seinem linken Rand kommt; er bleibt offen, solange der Zeiger darin ist oder ein Menü daraus offen ist.", + "prefs.browser_shown": "Angezeigt" + }, + "es": { + "prefs.browser_reveal_on_hover": "Mostrar al pasar el puntero", + "prefs.browser_show_note": "Visible mantiene el navegador en pantalla. Mostrar al pasar el puntero lo oculta hasta que el puntero llega a su lugar o a la pestaña fina de su borde izquierdo, y sigue abierto mientras el puntero está dentro o hay un menú suyo abierto.", + "prefs.browser_shown": "Visible" + }, + "fr": { + "prefs.browser_reveal_on_hover": "Révéler au survol", + "prefs.browser_show_note": "Affiché garde le navigateur à l'écran. Révéler au survol le cache jusqu'à ce que le pointeur passe sur sa place ou sur le fin onglet à son bord gauche, et il reste ouvert tant que le pointeur est dedans ou qu'un de ses menus est ouvert.", + "prefs.browser_shown": "Affiché" + }, + "he": { + "prefs.browser_reveal_on_hover": "הצגה בריחוף", + "prefs.browser_show_note": "״מוצג״ משאיר את הדפדפן על המסך. ״הצגה בריחוף״ מסתיר אותו עד שהמצביע מגיע למקומו או ללשונית הדקה בקצהו השמאלי, והוא נשאר פתוח כל עוד המצביע בתוכו או שתפריט ממנו פתוח.", + "prefs.browser_shown": "מוצג" + }, + "ja": { + "prefs.browser_reveal_on_hover": "ホバーで表示", + "prefs.browser_show_note": "「表示」はブラウザーを常に画面に表示します。「ホバーで表示」はポインターがその位置か左端の細いタブに来るまで隠し、ポインターが中にある間やそのメニューが開いている間は表示したままにします。", + "prefs.browser_shown": "表示" + }, + "vi": { + "prefs.browser_reveal_on_hover": "Hiện khi di chuột", + "prefs.browser_show_note": "Hiển thị giữ trình duyệt trên màn hình. Hiện khi di chuột ẩn nó cho đến khi con trỏ đến vị trí của nó hoặc thẻ mảnh ở cạnh trái, và nó vẫn mở khi con trỏ ở bên trong hoặc một menu của nó đang mở.", + "prefs.browser_shown": "Hiển thị" + }, + "zh": { + "prefs.browser_reveal_on_hover": "悬停时显示", + "prefs.browser_show_note": "“显示”让浏览器始终留在屏幕上。“悬停时显示”会将其隐藏,直到指针移到它的位置或其左边缘的细标签上;指针在其中或其菜单打开时,它保持打开。", + "prefs.browser_shown": "显示" + } +} diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index fe2a4acc45..bd1483e1da 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -101,8 +101,6 @@ pub fn drawn_occurrences() -> Vec<(Vec, String, Rect, bool)> { /// Browser state kept between frames. #[derive(Default)] pub struct TreeState { - /// The floating browser card is folded to its header. - pub panel_collapsed: bool, /// Geometry and assembly queries, independent of the camera, selection and row visibility. model: Option, /// Folders closed by the user (keys like `c0/bodies`). @@ -809,6 +807,53 @@ pub(crate) fn collapsed_width() -> f32 { t.space_24 + t.space_10 } +/// How the browser card shows (Preferences › Interface › Layout, #8692). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BrowserShow { + /// Always on screen (folded to its header when the user folds it). + #[default] + Shown, + /// Out of sight until the pointer comes over its place or the strip at its left edge, as the + /// navigation bar's "Hide until hovered"; never folded, so the whole card is the target. + Hover, +} + +/// Does the browser hide until hovered? Only its own card does: not the one sharing its column +/// with History, nor the Render page's. +pub(crate) fn reveals(app: &SolveApp) -> bool { + app.preferences.browser_show == BrowserShow::Hover && !crate::timeline_left::stacked(app) && !crate::render_workspace::active(app) +} + +/// How much of the browser set to reveal on hover shows this frame (0 hidden, 1 shown). The +/// pointer over last frame's card or the strip at its left edge shows it; a menu, popup, drag or +/// text edit started while it showed keeps it shown. A drag started elsewhere (orbiting the view) +/// passes over it without showing it. +fn reveal_opacity(app: &SolveApp, ctx: &egui::Context, previous: Option, edge: Rect) -> f32 { + let card = egui::Id::new("sc_browser_card"); + let memory = card.with("reveal"); + let was = ctx.data(|d| d.get_temp::(memory)).unwrap_or(false); + let over = ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| { + let layer = ctx.layer_id_at(p); + let on_card = previous.is_some_and(|r| r.contains(p)) && layer.is_some_and(|l| l.id == card); + let on_edge = edge.contains(p) && layer.is_none_or(|l| l.order == egui::Order::Background); + on_card || on_edge + }); + let dragging = ctx.dragged_id().is_some(); + let busy = app.viewport.context_menu.is_some() || egui::Popup::is_any_open(ctx) || dragging || ctx.egui_wants_keyboard_input(); + let shown = (over && (was || !dragging)) || (was && busy); + ctx.data_mut(|d| d.insert_temp(memory, shown)); + ctx.animate_value_with_time(memory.with("opacity"), f32::from(u8::from(shown)), crate::ui_kit::NAV_FADE_SECONDS) +} + +/// The thin tab at the hidden browser's left edge that says where it is. +fn reveal_tab(ui: &egui::Ui, card: Rect, opacity: f32) { + let t = Tokens::get(); + let x = card.left() + t.space_3; + let tab = Rect::from_min_max(pos2(x, card.top() + t.space_8), pos2(x + t.space_3, card.top() + t.space_8 + t.space_24 * 2.0)); + ui.painter().rect_filled(tab, t.space_3 / 2.0, t.text_dim.gamma_multiply(opacity)); +} + /// One content-hugging browser; the shared container supplies floating or flush compact chrome. pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) { let available = ui.available_rect_before_wrap(); @@ -839,12 +884,21 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) { { available.min.x = (history.right() + gap).min(available.right()) - gap; } - let collapsed = vertical && !stacked && app.preferences.timeline_left_browser_collapsed; + let reveals = reveals(app); + let collapsed = vertical && !stacked && !reveals && app.preferences.timeline_left_browser_collapsed; let bounds = if stacked { crate::timeline_left::stacked_rects(app, available).0 } else { panel_bounds(available, if vertical { None } else { app.shell.history }, if collapsed { collapsed_width() } else { width }, gap) }; + let opacity = if reveals { + let edge = Rect::from_min_max(pos2(available.left(), bounds.top()), pos2(bounds.left() + t.space_10, bounds.bottom())); + crate::scenario::publish_target("browser:edge", edge); + reveal_opacity(app, ui.ctx(), app.shell.browser, edge) + } else { + 1.0 + }; + app.shell.browser_hidden = reveals && opacity < 0.5; app.shell.browser = None; let min_height = HEADER_H + 2.0 * (CARD_INSET + 1.0); // A dialog in the left column holds the browser's place (`dialog_column`). @@ -855,19 +909,27 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) { let inner_height = (bounds.height() - frame.total_margin().sum().y).max(0.0); let card = Container::new(ui.ctx(), Surface::Panel).area(egui::Id::new("sc_browser_card")).fixed_pos(bounds.min).show(ui.ctx(), |ui| { let limits = Rect::from_min_max(available.min, pos2(available.right(), bounds.bottom())); - let response = container.show_frame(ui, frame, limits, |ui| { - ui.set_width(inner_width); - ui.set_max_height(inner_height); - if stacked { - ui.set_min_height(inner_height); - ui.set_clip_rect(ui.clip_rect().intersect(Rect::from_min_size(ui.max_rect().min, vec2(inner_width, inner_height)))); - } - if collapsed { - browser_collapsed_rail(app, ui); - } else { - browser_contents(app, ui, &mut acts, true); - } - }); + let response = ui + .scope(|ui| { + ui.set_opacity(opacity); + container.show_frame(ui, frame, limits, |ui| { + ui.set_width(inner_width); + ui.set_max_height(inner_height); + if stacked { + ui.set_min_height(inner_height); + ui.set_clip_rect(ui.clip_rect().intersect(Rect::from_min_size(ui.max_rect().min, vec2(inner_width, inner_height)))); + } + if collapsed { + browser_collapsed_rail(app, ui); + } else { + browser_contents(app, ui, &mut acts, true); + } + }) + }) + .inner; + if reveals && opacity < 1.0 { + reveal_tab(ui, response.response.rect, 1.0 - opacity); + } if !collapsed { resize_column(app, ui, response.response.rect, width, "left-browser-rail"); } @@ -1010,7 +1072,7 @@ fn browser_contents(app: &mut SolveApp, ui: &mut egui::Ui, acts: &mut Actions, c (app.preferences.timeline_left_browser_collapsed && app.shell.history_folding.is_none_or(|p| p[0] >= 1.0)) || ui.available_height() <= t.space_3 } else { - app.tree.panel_collapsed && !(app.ui.show_timeline && app.preferences.timeline_left) + app.preferences.browser_collapsed && !reveals(app) && !(app.ui.show_timeline && app.preferences.timeline_left) }) { return; @@ -1031,15 +1093,18 @@ fn browser_contents(app: &mut SolveApp, ui: &mut egui::Ui, acts: &mut Actions, c } /// The header: the title, the body count and (on the floating card) a chevron that folds the -/// card to just this row. The whole row is the button. +/// card to just this row. The whole row is the button. A browser that reveals on hover never +/// folds: the whole card is what the pointer finds. fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) { + let collapsible = collapsible && !reveals(app); let t = Tokens::get(); let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER_H), Sense::hover()); let response = ui.interact(rect, ui.id().with("browser_header"), if collapsible { Sense::click() } else { Sense::hover() }); crate::scenario::publish_target("browser:header", rect); let vertical = app.ui.show_timeline && app.preferences.timeline_left; let stacked = crate::timeline_left::stacked(app); - let collapsed = collapsible && if stacked { app.preferences.timeline_left_browser_collapsed } else { !vertical && app.tree.panel_collapsed }; + let collapsed = + collapsible && if stacked { app.preferences.timeline_left_browser_collapsed } else { !vertical && app.preferences.browser_collapsed }; if vertical { crate::scenario::publish_handle("left-browser-toggle", rect.center()); } @@ -1069,7 +1134,7 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) { } else if vertical { app.preferences.timeline_left_browser_collapsed = true; } else { - app.tree.panel_collapsed = !app.tree.panel_collapsed; + app.preferences.browser_collapsed = !app.preferences.browser_collapsed; } } } diff --git a/crates/ui-egui/src/browser_card_tests.rs b/crates/ui-egui/src/browser_card_tests.rs index 2bf76a96c5..b3404a7a8d 100644 --- a/crates/ui-egui/src/browser_card_tests.rs +++ b/crates/ui-egui/src/browser_card_tests.rs @@ -247,21 +247,109 @@ fn browser_header_folds_the_floating_card_to_its_header_and_back() { h.call("ui.resize", json!({"width":1400,"height":900})); h.settle(); let open = rect_of("browser:card").unwrap(); - assert!(!h.app.tree.panel_collapsed); + assert!(!h.app.preferences.browser_collapsed); let at = handle_at("browser:header").unwrap(); h.call("ui.click", json!({"x":at.x,"y":at.y})); h.settle(); let folded = rect_of("browser:card").unwrap(); - assert!(h.app.tree.panel_collapsed); + assert!(h.app.preferences.browser_collapsed); assert!(folded.height() < 60.0 && folded.height() < open.height(), "{folded:?} {open:?}"); assert_eq!(folded.top(), open.top()); assert!(drawn_rows().is_empty() || handle_at("row:Plate").is_none()); h.call("ui.click", json!({"x":at.x,"y":at.y})); h.settle(); - assert!(!h.app.tree.panel_collapsed); + assert!(!h.app.preferences.browser_collapsed); assert!((rect_of("browser:card").unwrap().height() - open.height()).abs() < 1.0); } +/// A folded browser stays folded in the next run, as the toolbar's pins do (#8692). +#[test] +fn the_folded_browser_is_remembered_across_runs() { + let mut h = Harness::new(); + h.call("ui.resize", json!({"width":1400,"height":900})); + h.settle(); + let open = rect_of("browser:card").unwrap(); + let at = handle_at("browser:header").unwrap(); + h.call("ui.click", json!({"x":at.x,"y":at.y})); + h.settle(); + assert!(h.app.preferences.browser_collapsed); + let saved = h.app.prefs(); + + let mut next = Harness::new(); + next.app.load_prefs(&saved); + assert!(next.app.preferences.browser_collapsed, "the fold round-trips through the settings"); + next.call("ui.resize", json!({"width":1400,"height":900})); + next.settle(); + let folded = rect_of("browser:card").unwrap(); + assert!(folded.height() < 60.0 && folded.height() < open.height(), "the next run starts folded: {folded:?}"); + let at = handle_at("browser:header").unwrap(); + next.call("ui.click", json!({"x":at.x,"y":at.y})); + next.settle(); + let mut after = SolveApp::new(Default::default(), Default::default()); + after.load_prefs(&next.app.prefs()); + assert!(!after.preferences.browser_collapsed, "unfolding is remembered too"); +} + +/// Reveal on hover (#8692): the browser is out of sight until the pointer comes over the strip at +/// its left edge or its place, stays while the pointer is inside it or its menu is open, and +/// never folds, even if it was folded before. +#[test] +fn a_browser_that_reveals_on_hover_shows_and_hides_with_the_pointer() { + for mode in [LayoutMode::Floating, LayoutMode::Docked] { + let mut h = Harness::new(); + h.app.ui.layout = mode; + h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"operation":"new_body","body_name":"One"})).unwrap(); + h.call("ui.resize", json!({"width":1400,"height":900})); + h.settle(); + let shown = rect_of("browser:card").unwrap(); + h.app.preferences.browser_collapsed = true; + h.app.preferences.browser_show = BrowserShow::Hover; + let away = json!({"x":900,"y":450}); + h.call("ui.move", away.clone()); + h.settle(); + assert!(h.app.shell.browser_hidden, "{mode:?}: out of sight while the pointer is elsewhere"); + let card = rect_of("browser:card").unwrap(); + assert!((card.height() - shown.height()).abs() < 1.0, "{mode:?}: the card keeps its unfolded place: {card:?} {shown:?}"); + + let edge = handle_at("browser:edge").unwrap(); + h.call("ui.move", json!({"x":edge.x,"y":edge.y})); + h.settle(); + assert!(!h.app.shell.browser_hidden, "{mode:?}: the left edge reveals it"); + let inside = rect_of("browser:card").unwrap().center(); + h.call("ui.move", json!({"x":inside.x,"y":inside.y})); + h.settle(); + assert!(!h.app.shell.browser_hidden, "{mode:?}: it stays while the pointer is inside"); + h.call("ui.move", away.clone()); + h.settle(); + assert!(h.app.shell.browser_hidden, "{mode:?}: leaving hides it again"); + + h.call("ui.move", json!({"x":inside.x,"y":inside.y})); + h.settle(); + let header = handle_at("browser:header").unwrap(); + h.call("ui.click", json!({"x":header.x,"y":header.y})); + h.settle(); + assert!(rect_of("browser:card").unwrap().height() > 60.0, "{mode:?}: the header does not fold it"); + h.step(&json!({"click":{"handle":"row:One"},"button":"right"})).unwrap(); + assert!(h.app.viewport.context_menu.is_some(), "{mode:?}: the row's menu opens"); + h.call("ui.move", away.clone()); + h.settle(); + assert!(!h.app.shell.browser_hidden, "{mode:?}: its menu keeps it shown"); + h.step(&json!({"key":"Escape"})).unwrap(); + h.call("ui.move", away.clone()); + h.settle(); + assert!(h.app.shell.browser_hidden, "{mode:?}: closing the menu away from it hides it"); + + h.app.preferences.browser_show = BrowserShow::Shown; + h.settle(); + assert!(!h.app.shell.browser_hidden); + assert!(rect_of("browser:card").unwrap().height() < 60.0, "{mode:?}: Shown folds it again as it was left"); + let mut restored = SolveApp::new(Default::default(), Default::default()); + h.app.preferences.browser_show = BrowserShow::Hover; + restored.load_prefs(&h.app.prefs()); + assert_eq!(restored.preferences.browser_show, BrowserShow::Hover); + } +} + /// Tiny or odd window sizes never panic: the cards leave out what cannot fit. #[test] fn tiny_floating_windows_never_panic() { diff --git a/crates/ui-egui/src/dialog_column.rs b/crates/ui-egui/src/dialog_column.rs index 7702b19324..33fc35ecb1 100644 --- a/crates/ui-egui/src/dialog_column.rs +++ b/crates/ui-egui/src/dialog_column.rs @@ -65,7 +65,7 @@ pub(crate) fn rect(app: &SolveApp, available: Rect, width: f32) -> Rect { let translucent = panel.floating() && app.preferences.browser_float; if app.ui.show_browser && !stacked && !translucent { let from = if history { right + gap } else { left }; - let collapsed = history && app.preferences.timeline_left_browser_collapsed; + let collapsed = history && app.preferences.timeline_left_browser_collapsed && !crate::browser::reveals(app); right = from + if collapsed { crate::browser::collapsed_width() } else { browser_width }; } let bottom = app.shell.history.filter(|_| app.ui.show_timeline && !app.preferences.timeline_left).map_or(available.bottom(), |h| h.top()) - gap; diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 1755da0fc1..a1bc93599d 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -1073,6 +1073,12 @@ preference_settings! { /// Draw the parts browser on a translucent card over the 3D view instead of a column of its /// own, so the workspace keeps the whole window width. browser_float: bool = false, PAGE_INTERFACE; + /// The browser card folded to its header with the chevron in that header, kept from one run + /// to the next like the toolbar's pins (#8692). + browser_collapsed: bool = false, PAGE_INTERFACE; + /// Whether the browser card is always shown or kept out of sight until the pointer comes to + /// it, as the navigation bar's "Hide until hovered" does (#8692). + browser_show: crate::browser::BrowserShow = Default::default(), PAGE_INTERFACE; /// Where command dialogs open: at the view's edge, or in the left column in place of the /// browser and the History there (#6707). dialog_place: crate::dialog_column::DialogPlace = Default::default(), PAGE_INTERFACE; @@ -4761,6 +4767,8 @@ mod tests { ("history_sketches", json!("under_feature"), PAGE_INTERFACE), ("history_play_button", json!(false), PAGE_INTERFACE), ("browser_float", json!(true), PAGE_INTERFACE), + ("browser_collapsed", json!(true), PAGE_INTERFACE), + ("browser_show", json!("hover"), PAGE_INTERFACE), ("dialog_place", json!("left_column"), PAGE_INTERFACE), ("color_vision", json!("protan"), PAGE_INTERFACE), ("viewport_theme", json!("light"), PAGE_INTERFACE), diff --git a/crates/ui-egui/src/prefs/interface.rs b/crates/ui-egui/src/prefs/interface.rs index c9d0572bcf..5396f3735c 100644 --- a/crates/ui-egui/src/prefs/interface.rs +++ b/crates/ui-egui/src/prefs/interface.rs @@ -208,6 +208,20 @@ pub(super) fn interface(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { } }); }); + // Out of sight until the pointer comes to it (#8692); the browser sharing its + // column with History keeps its place. + ui_kit::row(ui, "", |ui| { + use crate::browser::BrowserShow; + ui.add_enabled_ui(!crate::timeline_left::stacked(app), |ui| { + let mut selected = usize::from(app.preferences.browser_show == BrowserShow::Hover); + let options = [app.tr("prefs.browser_shown"), app.tr("prefs.browser_reveal_on_hover")]; + let segments = ui.scope(|ui| ui_kit::segmented(ui, &mut selected, &options, Default::default())); + segments.response.on_hover_text(app.tr("prefs.browser_show_note")); + if segments.inner { + app.preferences.browser_show = if selected == 1 { BrowserShow::Hover } else { BrowserShow::Shown }; + } + }); + }); // Where command dialogs open (#6707). ui_kit::row(ui, app.tr("prefs.dialogs"), |ui| { use crate::dialog_column::DialogPlace; diff --git a/crates/ui-egui/src/render_workspace_library.rs b/crates/ui-egui/src/render_workspace_library.rs index 6e31e1d1fc..12d6a7a3d5 100644 --- a/crates/ui-egui/src/render_workspace_library.rs +++ b/crates/ui-egui/src/render_workspace_library.rs @@ -321,7 +321,7 @@ pub(super) fn open_category(app: &mut SolveApp, kind: &str) { }; app.ui.show_browser = true; let _ = app.run("view.render_browser", json!({"open":true})); - app.tree.panel_collapsed = false; + app.preferences.browser_collapsed = false; } pub(super) fn choose(app: &mut SolveApp, view: &mut View, kind: &str, id: &str) -> Result<(), String> { switch_kind(view, kind); diff --git a/crates/ui-egui/src/render_workspace_tests.rs b/crates/ui-egui/src/render_workspace_tests.rs index 865aa4e049..ea8a43c700 100644 --- a/crates/ui-egui/src/render_workspace_tests.rs +++ b/crates/ui-egui/src/render_workspace_tests.rs @@ -3794,7 +3794,7 @@ fn render_workspace_focus_restores_docks_without_design_history_or_camera_change h.app.preferences.render_browser_width = 350.0; h.app.preferences.render_inspector_width = 390.0; h.app.preferences.render_shots_strip = shots; - h.app.tree.panel_collapsed = folded; + h.app.preferences.browser_collapsed = folded; h.app.shell.render_browser_open = folded; h.frames(3); let dock_preferences = h.app.prefs(); @@ -3829,7 +3829,7 @@ fn render_workspace_focus_restores_docks_without_design_history_or_camera_change assert_eq!(restored_edge.as_ref().map(std::mem::discriminant), edge.as_ref().map(std::mem::discriminant)); } assert_eq!(h.app.prefs(), dock_preferences); - assert_eq!(h.app.tree.panel_collapsed, folded); + assert_eq!(h.app.preferences.browser_collapsed, folded); assert_eq!(h.app.shell.render_browser_open, folded); assert_eq!((h.app.cam, h.app.session.camera), camera); assert!(Arc::ptr_eq(&h.app.session.doc, &document)); diff --git a/crates/ui-egui/src/shell.rs b/crates/ui-egui/src/shell.rs index 8481f86d06..5303e6cf3e 100644 --- a/crates/ui-egui/src/shell.rs +++ b/crates/ui-egui/src/shell.rs @@ -99,6 +99,9 @@ pub(crate) struct State { /// Actual floating workspace surfaces and their optional canvas reservation. pub workspace_panels: std::collections::BTreeMap<&'static str, (Rect, Option)>, pub browser: Option, + /// The browser set to reveal on hover is out of sight this frame (#8692); its place stays + /// in `browser`, which the pointer reveals. + pub browser_hidden: bool, pub history: Option, /// In-memory seam drag width, never serialized as a preference. pub history_drag_width: Option, diff --git a/crates/ui-egui/tests/disclosure_render.rs b/crates/ui-egui/tests/disclosure_render.rs index 284358454f..f76dc06a61 100644 --- a/crates/ui-egui/tests/disclosure_render.rs +++ b/crates/ui-egui/tests/disclosure_render.rs @@ -32,7 +32,7 @@ fn appearance_and_browser_folds_keep_handles_and_render_both_states() { h.step(&json!({"click":{"handle":"browser:header"}})).unwrap(); } assert_eq!(h.app.dialog.as_ref().unwrap().collapsed, !open); - assert_eq!(h.app.tree.panel_collapsed, !open); + assert_eq!(h.app.preferences.browser_collapsed, !open); assert_eq!(scenario::handle_at("fold:Bodies").is_some(), open); assert_eq!(scenario::handle_at("appearance:search").is_some(), open); // Fresh context transfers all textures to the software renderer, including fonts.