collective

Browse / github.com/storytold/wordcraft / patch

Navigation pane: its views use the shared dialog tabs

Navigation pane: its views use the shared dialog tabs The Navigation pane showed Headings, Pages and Results as plain selectable labels. It now uses the shared `widgets::dialog_tabs` control, so the look and hover match the dialog tabs. `dialog_tabs_within` is the same control with padding that shrinks to fit a narrow pane (`dialog_tabs` is unchanged for dialogs). Kept: the selected tab, the switch to Results while typing a search, and the translated labels. Colours come from `theme::Tokens`. No existing issue or PR covers this (searched `storytold/wordcraft`). Tests: new `panes::tests::navigation_tabs_switch_the_view` clicks each tab. `

Status
open
Score
0.00
Adopters
0 other operators
Author
fluorine (imported from pull request #4480, open)
Base
2cf1d6c7ae94723a45bd6e7e8c884fe375148750
Size
+35 −11 in 4 files
Review
pending
Shared
2026-10-28
Id
ebe597a85248941d8edaed1c

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/panes/nav.rs crates/ui-egui/src/panes/tests.rs crates/ui-egui/src/widgets.rs crates/ui-egui/src/widgets/icon_buttons.rs

diff --git a/crates/ui-egui/src/panes/nav.rs b/crates/ui-egui/src/panes/nav.rs--- a/crates/ui-egui/src/panes/nav.rs+++ b/crates/ui-egui/src/panes/nav.rs@@ -29,14 +29,13 @@ pub(super) fn nav(app: &mut WordApp, ui: &mut Ui) {         }     }     ui.add_space(4.0);-    ui.horizontal_wrapped(|ui| {-        for (k, l) in [("headings", "Headings"), ("pages", "Pages"), ("results", "Results")] {-            if ui.selectable_label(app.ui.nav_tab == k, tl!(l)).clicked() {-                app.ui.nav_tab = k.into();-            }-        }-    });-    ui.separator();+    let keys = ["headings", "pages", "results"];+    let names = ["Headings", "Pages", "Results"].map(|n| tl!(n).to_string());+    let current = keys.iter().position(|k| *k == app.ui.nav_tab).unwrap_or(0);+    if let Some(i) = crate::widgets::dialog_tabs_within(ui, &names, current, ui.available_width()) {+        app.ui.nav_tab = keys[i].into();+    }+    ui.add_space(4.0);     egui::ScrollArea::vertical().show(ui, |ui| match app.ui.nav_tab.as_str() {         "pages" => {             let l = app.session.layout();diff --git a/crates/ui-egui/src/panes/tests.rs b/crates/ui-egui/src/panes/tests.rs--- a/crates/ui-egui/src/panes/tests.rs+++ b/crates/ui-egui/src/panes/tests.rs@@ -64,6 +64,21 @@ fn f6_past_the_last_pane_types_into_the_document() {     assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body).trim(), "typed"); } +/// The Navigation pane's views use the shared dialog tabs and each click selects that view.+#[test]+#[cfg(not(target_arch = "wasm32"))]+fn navigation_tabs_switch_the_view() {+    use egui_kittest::kittest::Queryable;+    let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("Open it")), crate::Services::default());+    app.run("view.navigationPane", serde_json::json!({"value": true})).unwrap();+    let mut h = pane_harness(app);+    for (label, key) in [("Pages", "pages"), ("Results", "results"), ("Headings", "headings")] {+        h.get_by_label(label).click();+        h.run_steps(3);+        assert_eq!(h.state().ui.nav_tab, key, "{label} tab");+    }+}+ fn pane_harness(app: crate::WordApp) -> egui_kittest::Harness<'static, crate::WordApp> {     let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(         |ui, app: &mut crate::WordApp| {diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs--- a/crates/ui-egui/src/widgets.rs+++ b/crates/ui-egui/src/widgets.rs@@ -26,7 +26,8 @@ pub use combo::{ use combo::{Row, highlight_row, section_row, section_rows}; pub use groups::{command_icon, command_menu, customize_menu_items, group, group_titled, row}; pub use icon_buttons::{-    dialog_tabs, icon_button, icon_menu_item, icon_only_button, icon_text_button, icon_text_button_width, icon_text_menu_button, primary_button,+    dialog_tabs, dialog_tabs_within, icon_button, icon_menu_item, icon_only_button, icon_text_button, icon_text_button_width, icon_text_menu_button,+    primary_button, }; #[cfg(test)] use tooltip::show_hint;diff --git a/crates/ui-egui/src/widgets/icon_buttons.rs b/crates/ui-egui/src/widgets/icon_buttons.rs--- a/crates/ui-egui/src/widgets/icon_buttons.rs+++ b/crates/ui-egui/src/widgets/icon_buttons.rs@@ -108,10 +108,19 @@ pub fn icon_menu_item(ui: &mut Ui, icon: &str, text: &str, shortcut: &str, min_w /// A strip of dialog tabs. The active tab joins the page below it. /// Returns the index of the tab the user clicked. pub fn dialog_tabs(ui: &mut Ui, labels: &[String], selected: usize) -> Option<usize> {+    dialog_tabs_within(ui, labels, selected, f32::INFINITY)+}++/// `dialog_tabs` for a narrow pane: the tab padding shrinks, to 4 pt+/// per side at least, so the strip fits `max_width`.+pub fn dialog_tabs_within(ui: &mut Ui, labels: &[String], selected: usize, max_width: f32) -> Option<usize> {     const H: f32 = 26.0;     let t = Tokens::get(ui.ctx());     let page = ui.visuals().window_fill;-    let need: f32 = labels.iter().map(|l| ui.painter().layout_no_wrap(l.clone(), regular(12.5), t.text).size().x + 27.0).sum::<f32>() + 1.0;+    let texts: Vec<f32> = labels.iter().map(|l| ui.painter().layout_no_wrap(l.clone(), regular(12.5), t.text).size().x).collect();+    let spare = (max_width - 1.0 - texts.iter().sum::<f32>()) / texts.len().max(1) as f32;+    let pad = spare.clamp(8.0, 28.0);+    let need: f32 = texts.iter().map(|w| w + pad - 1.0).sum::<f32>() + 1.0;     ui.set_min_width(need);     let origin = ui.cursor().min;     let width = ui.available_width();@@ -123,7 +132,7 @@ pub fn dialog_tabs(ui: &mut Ui, labels: &[String], selected: usize) -> Option<us     for (i, label) in labels.iter().enumerate() {         let on = i == selected;         let galley = ui.painter().layout_no_wrap(label.clone(), regular(12.5), t.text);-        let w = galley.size().x + 28.0;+        let w = galley.size().x + pad;         let r = Rect::from_min_max(pos2(x, origin.y + 2.0), pos2(x + w, base_y));         let resp = ui.interact(r, ui.scope_id().with(("dialog_tab", i)), Sense::click());         resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, on, label));