mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Sheet tabs: All Sheets navigator with a filter field (fixes #839)
This commit is contained in:
@@ -113,6 +113,8 @@ pub enum Icon {
|
||||
/// Light/dark appearance: a circle, its right half filled.
|
||||
Appearance,
|
||||
Sheet,
|
||||
/// All Sheets navigator next to the sheet tabs: a list of rows with a bullet each.
|
||||
SheetList,
|
||||
Book,
|
||||
Folder,
|
||||
/// Header & Footer: a page with its top / bottom band marked.
|
||||
@@ -419,6 +421,12 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, c: Color32) {
|
||||
g.line(&[(2.0, 8.0), (8.0, 2.5), (14.0, 8.0)], c, 1.2);
|
||||
g.line(&[(4.0, 6.5), (4.0, 13.5), (12.0, 13.5), (12.0, 6.5)], c, 1.2);
|
||||
}
|
||||
Icon::SheetList => {
|
||||
for y in [4.0, 8.0, 12.0] {
|
||||
g.rect(2.5, y - 1.0, 2.0, 2.0, Some(c), None);
|
||||
g.stroke(&[(6.5, y), (13.5, y)], c);
|
||||
}
|
||||
}
|
||||
Icon::More => {
|
||||
for x in [4.0, 8.0, 12.0] {
|
||||
g.circle(x, 8.0, 1.0, Some(c), None);
|
||||
|
||||
@@ -367,7 +367,7 @@ struct ComboState {
|
||||
}
|
||||
|
||||
/// What the user did with an [`editable_combo`].
|
||||
enum ComboResult {
|
||||
pub(crate) enum ComboResult {
|
||||
None,
|
||||
/// A row was applied: clicked or Enter on the highlighted row.
|
||||
Picked(String),
|
||||
@@ -378,7 +378,7 @@ enum ComboResult {
|
||||
/// Excel-style dropdown: a filter field (type to narrow the list) plus arrow/Enter selection.
|
||||
/// `items` are `(value, display)`; the value is what comes back. When `forced` (keytip invoke)
|
||||
/// or clicked, the popup opens, focuses the filter field, and selects its text.
|
||||
fn editable_combo(
|
||||
pub(crate) fn editable_combo(
|
||||
ui: &mut Ui,
|
||||
anchor: &egui::Response,
|
||||
salt: &'static str,
|
||||
|
||||
+46
-11
@@ -1,7 +1,9 @@
|
||||
//! Sheet tabs and the status bar.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use gridcraft_engine::model::Visibility;
|
||||
use gridcraft_engine::model::{Sheet, Visibility};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::SheetApp;
|
||||
@@ -14,6 +16,25 @@ fn rename_needs_focus(has_focus: bool, lost_focus: bool) -> bool {
|
||||
!has_focus && !lost_focus
|
||||
}
|
||||
|
||||
/// Rows of the All Sheets navigator as `(workbook index, name)`. Hidden sheets stay out, like on the tab strip:
|
||||
/// Unhide is the way to reach them.
|
||||
fn navigator_items(sheets: &[Arc<Sheet>]) -> Vec<(String, String)> {
|
||||
sheets.iter().enumerate().filter(|(_, s)| s.visibility == Visibility::Visible).map(|(i, s)| (i.to_string(), s.name.clone())).collect()
|
||||
}
|
||||
|
||||
/// Switch to sheet `i` from the tab strip or the navigator. In point mode the formula being typed keeps going on the
|
||||
/// other sheet; otherwise the edit is committed first.
|
||||
fn activate_sheet(app: &mut SheetApp, i: usize) {
|
||||
if app.editor.as_ref().is_some_and(|e| e.can_point()) {
|
||||
let _ = app.session.run("sheet.activate", json!({"sheet": i}));
|
||||
} else {
|
||||
if app.editor.is_some() {
|
||||
app.commit_edit(0, 0, false, false);
|
||||
}
|
||||
app.run_or_alert("sheet.activate", json!({"sheet": i}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Panel::bottom("sheet_tabs").exact_size(32.0).frame(egui::Frame::NONE.fill(t.tab_bar)).show(ui, |ui| {
|
||||
@@ -28,6 +49,16 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
||||
if crate::widgets::icon_button(ui, Icon::Right, t.text_dim, "Next sheet", vec2(24.0, 24.0)).clicked() {
|
||||
app.run_or_alert("sheet.next", json!({}));
|
||||
}
|
||||
let all = crate::widgets::icon_button(ui, Icon::SheetList, t.text_dim, "All sheets", vec2(24.0, 24.0));
|
||||
all.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "All sheets"));
|
||||
let items = app.session.active().map(|d| navigator_items(&d.wb.sheets)).unwrap_or_default();
|
||||
let active = app.session.active().map(|d| d.wb.active_sheet.to_string()).unwrap_or_default();
|
||||
let rows: Vec<(&str, String)> = items.iter().map(|(i, name)| (i.as_str(), name.clone())).collect();
|
||||
if let crate::ribbon::ComboResult::Picked(i) = crate::ribbon::editable_combo(ui, &all, "sheet_navigator", &active, &rows, false)
|
||||
&& let Ok(i) = i.parse::<usize>()
|
||||
{
|
||||
activate_sheet(app, i);
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
let Some(d) = app.session.active() else { return };
|
||||
let sheets: Vec<(usize, String, Option<[u8; 3]>, bool)> =
|
||||
@@ -106,15 +137,7 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, &name, font, if active { t.accent } else { t.text });
|
||||
}
|
||||
if resp.clicked() {
|
||||
if app.editor.as_ref().is_some_and(|e| e.can_point()) {
|
||||
// Point mode across sheets: switch without committing.
|
||||
let _ = app.session.run("sheet.activate", json!({"sheet": i}));
|
||||
} else {
|
||||
if app.editor.is_some() {
|
||||
app.commit_edit(0, 0, false, false);
|
||||
}
|
||||
app.run_or_alert("sheet.activate", json!({"sheet": i}));
|
||||
}
|
||||
activate_sheet(app, i);
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
app.grid.renaming_tab = Some(i);
|
||||
@@ -332,7 +355,9 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::rename_needs_focus;
|
||||
use super::{navigator_items, rename_needs_focus};
|
||||
use gridcraft_engine::model::{Sheet, Visibility};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn rename_field_takes_focus_once_and_lets_go_on_commit() {
|
||||
@@ -340,4 +365,14 @@ mod tests {
|
||||
assert!(!rename_needs_focus(true, false), "already focused: do not ask again");
|
||||
assert!(!rename_needs_focus(false, true), "Enter or click-away: must not take focus back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_lists_visible_sheets_by_workbook_index() {
|
||||
let mut hidden = Sheet::new("Hidden");
|
||||
hidden.visibility = Visibility::Hidden;
|
||||
let mut very_hidden = Sheet::new("VeryHidden");
|
||||
very_hidden.visibility = Visibility::VeryHidden;
|
||||
let sheets = [Sheet::new("Data"), hidden, very_hidden, Sheet::new("Summary")].map(Arc::new);
|
||||
assert_eq!(navigator_items(&sheets), vec![("0".to_string(), "Data".to_string()), ("3".to_string(), "Summary".to_string())]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! The All Sheets navigator next to the sheet tabs: type part of a name, Enter, and that sheet is active (#839).
|
||||
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> egui_kittest::Harness<'static, SheetApp> {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
for _ in 0..3 {
|
||||
s.run("home.insertSheet", json!({})).unwrap();
|
||||
}
|
||||
for (i, name) in ["Data", "Hidden", "Notes", "Summary"].iter().enumerate() {
|
||||
s.run("sheet.rename", json!({"sheet": i, "name": name})).unwrap();
|
||||
}
|
||||
s.run("sheet.hide", json!({"sheet": 1})).unwrap();
|
||||
s.run("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_a_name_and_enter_activates_that_sheet() {
|
||||
let mut h = harness();
|
||||
assert_eq!(h.state().session.active().unwrap().wb.active_sheet, 0);
|
||||
h.get_by_label("All sheets").click();
|
||||
h.run_steps(4);
|
||||
h.input_mut().events.push(Event::Text("summ".into()));
|
||||
h.run_steps(3);
|
||||
h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().session.active().unwrap().wb.active_sheet, 3, "Enter on the filtered row must activate Summary");
|
||||
}
|
||||
Reference in New Issue
Block a user