diff --git a/crates/ui-egui/src/macro_editor.rs b/crates/ui-egui/src/macro_editor.rs index 9af815675..24034c76e 100644 --- a/crates/ui-egui/src/macro_editor.rs +++ b/crates/ui-egui/src/macro_editor.rs @@ -790,14 +790,16 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu let home_title = home_title(app, &ed); let dirty_mark = if ed.dirty { " •" } else { "" }; let screen = ctx.content_rect(); + let fit = (screen.size() - vec2(16.0, 16.0)).max(vec2(240.0, 200.0)); let mut win_open = true; ed.refs.drawn = false; egui::Window::new(format!("Macro: {}{dirty_mark}", ed.draft.name)) .id(Id::new("gridcraft.macro_editor")) .collapsible(false) .resizable(true) - .default_size(vec2(840.0, 560.0)) - .min_size(vec2(660.0, 380.0)) + // Never larger than the app's window, so a narrow one keeps every control on screen. + .default_size(vec2(840.0, 560.0).min(fit)) + .min_size(vec2(660.0, 380.0).min(fit)) // To the right, so the data the overlays mark (usually top left) stays in view. .default_pos(pos2((screen.right() - 860.0).max(screen.left() + 20.0), screen.top() + 190.0)) .open(&mut win_open) @@ -1034,8 +1036,8 @@ fn prompts(app: &mut SheetApp, ctx: &egui::Context, ed: &mut Editor, open: &mut fn body(app: &mut SheetApp, ui: &mut Ui, d: &mut Dialog, ed: &mut Editor) { let t = Tokens::get(ui.ctx()); let debugging = app.session.macro_debug.is_some(); - // Name, description, Save. - ui.horizontal(|ui| { + // Name, description, Save; they wrap in a narrow window. + ui.horizontal_wrapped(|ui| { ui.label("Name:"); let name = ui.add_enabled(!debugging, egui::TextEdit::singleline(&mut ed.draft.name).desired_width(180.0).id_salt("me_name")); ui.label("Description:"); @@ -1055,7 +1057,8 @@ fn body(app: &mut SheetApp, ui: &mut Ui, d: &mut Dialog, ed: &mut Editor) { status_line(app, ui, ed, &t); ui.separator(); let avail = ui.available_size(); - let left_w = (avail.x * 0.52).max(300.0); + // The step list takes about half; in a narrow window the inspector keeps room for its tabs. + let left_w = (avail.x * 0.52).max(300.0).min((avail.x - 190.0).max(160.0)); ui.horizontal_top(|ui| { ui.allocate_ui_with_layout(vec2(left_w, avail.y), egui::Layout::top_down(egui::Align::Min), |ui| { ui.set_min_size(vec2(left_w, avail.y)); @@ -1067,7 +1070,7 @@ fn body(app: &mut SheetApp, ui: &mut Ui, d: &mut Dialog, ed: &mut Editor) { }); ui.separator(); ui.allocate_ui_with_layout(vec2(ui.available_width(), avail.y), egui::Layout::top_down(egui::Align::Min), |ui| { - egui::ScrollArea::vertical().id_salt("me_inspector").auto_shrink([false, false]).show(ui, |ui| { + egui::ScrollArea::both().id_salt("me_inspector").auto_shrink([false, false]).show(ui, |ui| { inspector(app, ui, d, ed, debugging); }); }); @@ -1076,34 +1079,33 @@ fn body(app: &mut SheetApp, ui: &mut Ui, d: &mut Dialog, ed: &mut Editor) { fn toolbar(app: &mut SheetApp, ui: &mut Ui, ed: &mut Editor, debugging: bool) { let t = Tokens::get(ui.ctx()); - ui.horizontal(|ui| { + // The buttons wrap onto a second row in a narrow window. + ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 2.0; let editing = !debugging; - if ui.add_enabled_ui(editing, |ui| small_button(ui, Icon::Plus, "Add Step", "Add a step after the selected one", false)).inner.clicked() { + if ui.add_enabled(editing, |ui: &mut Ui| small_button(ui, Icon::Plus, "Add Step", "Add a step after the selected one", false)).clicked() { ed.picker = Some(Picker::default()); } let has_step = ed.selected.as_deref().and_then(split_path).is_some(); if ui - .add_enabled_ui(editing && has_step, |ui| small_button(ui, Icon::Copy, "Duplicate", "Duplicate the selected step", false)) - .inner + .add_enabled(editing && has_step, |ui: &mut Ui| small_button(ui, Icon::Copy, "Duplicate", "Duplicate the selected step", false)) .clicked() { duplicate(ed); } - if ui.add_enabled_ui(editing && has_step, |ui| small_button(ui, Icon::Delete, "Delete", "Delete the selected step", false)).inner.clicked() { + if ui.add_enabled(editing && has_step, |ui: &mut Ui| small_button(ui, Icon::Delete, "Delete", "Delete the selected step", false)).clicked() { delete(ed); } sep(ui, &t); if ui - .add_enabled_ui(editing, |ui| { + .add_enabled(editing, |ui: &mut Ui| { small_button(ui, Icon::Check, "Check", "Check the inputs and show on the grid what the macro would change", false) }) - .inner .clicked() { check(app, ed); } - if ui.add_enabled_ui(editing, |ui| small_button(ui, Icon::Play, "Run", "Run the macro as one undo step", false)).inner.clicked() { + if ui.add_enabled(editing, |ui: &mut Ui| small_button(ui, Icon::Play, "Run", "Run the macro as one undo step", false)).clicked() { run(app, ed); } sep(ui, &t); @@ -1116,10 +1118,10 @@ fn toolbar(app: &mut SheetApp, ui: &mut Ui, ed: &mut Editor, debugging: bool) { debug_action(app, ed, "continue"); } let can_run_to = ed.selected.as_deref().and_then(split_path).is_some(); - if ui.add_enabled_ui(can_run_to, |ui| small_button(ui, Icon::RunTo, "Run to Here", "Run until the selected step", false)).inner.clicked() { + if ui.add_enabled(can_run_to, |ui: &mut Ui| small_button(ui, Icon::RunTo, "Run to Here", "Run until the selected step", false)).clicked() { debug_action(app, ed, "runTo"); } - if ui.add_enabled_ui(debugging, |ui| small_button(ui, Icon::StopSquare, "Stop", "Stop debugging", false)).inner.clicked() { + if ui.add_enabled(debugging, |ui: &mut Ui| small_button(ui, Icon::StopSquare, "Stop", "Stop debugging", false)).clicked() { ed.ask_stop = true; } }); @@ -1486,7 +1488,7 @@ fn picker(ui: &mut Ui, ed: &mut Editor) { // ---------------------------------------------------------------- the inspector fn inspector(app: &mut SheetApp, ui: &mut Ui, d: &mut Dialog, ed: &mut Editor, debugging: bool) { - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { for (tab, label) in [(Tab::Step, "Details"), (Tab::Inputs, "Inputs"), (Tab::Output, "Output")] { if ui.selectable_label(ed.tab == tab, label).clicked() { ed.tab = tab; diff --git a/crates/ui-egui/tests/it/macro_editor.rs b/crates/ui-egui/tests/it/macro_editor.rs index 418d36293..241c652b8 100644 --- a/crates/ui-egui/tests/it/macro_editor.rs +++ b/crates/ui-egui/tests/it/macro_editor.rs @@ -502,3 +502,31 @@ fn the_control_channels_cancel_closes_the_editor_as_its_own_close_does() { assert!(h.state().macros.editor.is_none()); assert!(h.state().grid.macro_overlay.is_none()); } + +#[test] +fn the_editor_fits_a_narrow_window() { + for size in [egui::vec2(600.0, 480.0), egui::vec2(480.0, 400.0)] { + let mut session = Session::new(); + session.new_workbook(); + let app = SheetApp::new(session, Default::default()); + let mut h = egui_kittest::Harness::builder().with_size(size).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(4); + three(&mut h); + h.run_steps(4); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, size); + let window = h.get_by_label_contains("Macro: Three").rect(); + assert!(screen.contains_rect(window), "the editor at {window:?} is outside the {size:?} window"); + // The editor's own controls (Delete is left out: the ribbon has one too). + for label in ["Add Step", "Duplicate", "Check", "Run", "Step", "Continue", "Run to Here", "Stop", "Details", "Inputs", "Output", "Save"] { + let r = h.get_by_label(label).rect(); + assert!(screen.contains_rect(r), "{label} at {r:?} is outside the {size:?} window"); + } + } +} diff --git a/crates/ui-egui/tests/it/macros_ui.rs b/crates/ui-egui/tests/it/macros_ui.rs index d7635d572..7c18b08ff 100644 --- a/crates/ui-egui/tests/it/macros_ui.rs +++ b/crates/ui-egui/tests/it/macros_ui.rs @@ -283,3 +283,34 @@ fn the_macros_dialog_offers_trust_for_scripts_that_came_with_a_file() { h.run_steps(3); assert_eq!(value(&h, "D4"), Value::Text("script".into())); } + +#[test] +fn the_macros_and_record_macro_dialogs_fit_a_narrow_window() { + for size in [egui::vec2(600.0, 480.0), egui::vec2(480.0, 400.0)] { + let mut s = Session::new(); + s.new_workbook(); + s.execute("macro.save", json!({"name": "Tidy", "steps": []})).unwrap(); + let app = SheetApp::new(s, Default::default()); + let mut h = egui_kittest::Harness::builder().with_size(size).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, size); + for (dialog, labels) in [("macros", &["Run", "Delete", "Options…", "Cancel"][..]), ("recordMacro", &["OK", "Cancel"][..])] { + h.state_mut().open_dialog(dialog, json!({})); + h.run_steps(4); + for label in labels { + // The dialog's own button: the last one drawn (the ribbon has a Delete too). + let r = h.get_all_by_label(label).last().map(|n| n.rect()).unwrap_or(egui::Rect::NOTHING); + assert!(screen.contains_rect(r), "{dialog}: {label} at {r:?} is outside the {size:?} window"); + } + h.state_mut().dialog = None; + h.run_steps(2); + } + } +} diff --git a/docs/macros.md b/docs/macros.md index a31ef5cc6..b315795ee 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -281,7 +281,8 @@ other macros open; a macro that was left out is not in the saved file. Run runs the selected macro on the selection or the selected script; Step Into, Edit and Create open the step editor; Delete asks first; Options sets a macro's shortcut letter and description. The dialog keeps the result of `view.macros`, as before macros existed. -- **Step editor.** A resizable window that works on a draft and saves it with `macro.save`. The +- **Step editor.** A resizable window that works on a draft and saves it with `macro.save`; in a + narrow app window it is no larger than the window and its toolbar wraps onto a second row. The step list shows each step's number, a breakpoint dot in the gutter (click to set or clear), a category icon, a summary, an error marker and an enabled checkbox; Repeat bodies and If branches are indented under their step, and rows drag to reorder within their list. Add Step opens a