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
@@ -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();@@ -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| {@@ -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;@@ -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));