mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-05 01:58:58 +00:00
#1246: drag the formula bar's bottom edge to resize it
The bottom edge of the formula bar is a resize handle: dragging it sets
the height in whole lines (up to half the window; one line collapses
the bar). Expanding again (the chevron, Ctrl+Shift+U) returns to the
dragged height instead of a fixed five lines. The drag previews live
and commits once through view.expandFormulaBar {lines}, which is also
scriptable; the height is kept with the UI preferences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
178f3eb745
commit
dea1359c7e
6 files changed
+160
-10
No files matched your search
@@ -163,8 +163,19 @@ impl SheetApp {
|
||||
Ok(Json::Null)
|
||||
}
|
||||
"view.expandFormulaBar" => {
|
||||
self.ui.formula_bar_expanded = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.formula_bar_expanded);
|
||||
Ok(json!({"on": self.ui.formula_bar_expanded}))
|
||||
// `lines` is where a drag of the bar's bottom edge left it: one line collapses it,
|
||||
// more expands it to that height, which expanding returns to afterwards.
|
||||
if let Some(lines) = p.get("lines").and_then(Json::as_u64) {
|
||||
let lines = usize::try_from(lines).unwrap_or(usize::MAX).clamp(1, crate::formula_bar::MAX_DRAG_LINES);
|
||||
self.ui.formula_bar_expanded = lines > 1;
|
||||
if lines > 1 {
|
||||
self.ui.formula_bar_lines = lines;
|
||||
}
|
||||
Ok(json!({"on": self.ui.formula_bar_expanded, "lines": self.ui.formula_bar_lines}))
|
||||
} else {
|
||||
self.ui.formula_bar_expanded = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.formula_bar_expanded);
|
||||
Ok(json!({"on": self.ui.formula_bar_expanded}))
|
||||
}
|
||||
}
|
||||
"view.darkMode" => {
|
||||
let current = if self.ui.system_theme { self.effective_dark } else { self.ui.dark };
|
||||
|
||||
@@ -263,7 +263,11 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
|
||||
("ui.ribbonTab", "Ribbon Tab", ""),
|
||||
("app.checkForUpdates", "Check for Updates…", ""),
|
||||
("view.collapseRibbon", "Collapse Ribbon", ""),
|
||||
("view.expandFormulaBar", "Expand Formula Bar", "{on?: bool} (toggles without it) → {on}"),
|
||||
(
|
||||
"view.expandFormulaBar",
|
||||
"Expand Formula Bar",
|
||||
"{on?: bool} (toggles without it) → {on}; {lines: int} sets the expanded height (1 collapses) → {on, lines}",
|
||||
),
|
||||
("ui.pythonMode", "Python Mode", "{on?: bool} (toggles without it) → {on}; a refused start is an error"),
|
||||
("view.darkMode", "Dark Mode", ""),
|
||||
("view.theme", "Display Theme", ""),
|
||||
|
||||
@@ -12,6 +12,11 @@ use crate::theme::{self, Tokens};
|
||||
|
||||
/// Lines the formula bar (and the in-cell editor) grows to before it scrolls.
|
||||
pub const MAX_LINES: usize = 5;
|
||||
/// The most lines a drag of the bar's bottom edge can give it (the window caps it at half its
|
||||
/// height first).
|
||||
pub const MAX_DRAG_LINES: usize = 100;
|
||||
/// How far the bar's bottom edge reaches up into the bar as a resize handle (its bottom margin).
|
||||
const HANDLE: f32 = 4.0;
|
||||
/// The bar's height at one line.
|
||||
const ONE_LINE: f32 = 30.0;
|
||||
/// What the bar adds around the field's rows: the panel's margins (3 + 3), the field's (3 + 3)
|
||||
@@ -40,11 +45,22 @@ pub struct BarLayout {
|
||||
pub visible_lines: usize,
|
||||
/// The caret while editing in the bar.
|
||||
pub caret: Option<Rect>,
|
||||
/// The bottom edge that resizes the bar.
|
||||
pub handle: Rect,
|
||||
}
|
||||
|
||||
impl Default for BarLayout {
|
||||
fn default() -> Self {
|
||||
BarLayout { name_box: Rect::NOTHING, buttons: Rect::NOTHING, field: Rect::NOTHING, height: 0.0, lines: 1, visible_lines: 1, caret: None }
|
||||
BarLayout {
|
||||
name_box: Rect::NOTHING,
|
||||
buttons: Rect::NOTHING,
|
||||
field: Rect::NOTHING,
|
||||
height: 0.0,
|
||||
lines: 1,
|
||||
visible_lines: 1,
|
||||
caret: None,
|
||||
handle: Rect::NOTHING,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +89,15 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
let font = theme::ui_font(13.0);
|
||||
let row_h = ui.ctx().fonts_mut(|f| f.row_height(&font));
|
||||
let last = BarLayout::get(ui.ctx());
|
||||
// Height follows the content's line count (last frame's), 1 to MAX_LINES; the chevron pins it
|
||||
// at MAX_LINES. It eases only when that target changes.
|
||||
let lines = if app.ui.formula_bar_expanded { MAX_LINES } else { last.lines.clamp(1, MAX_LINES) };
|
||||
// Height follows the content's line count (last frame's), 1 to MAX_LINES; expanded (the
|
||||
// chevron), it is the height the bottom edge was dragged to. It eases only when that target
|
||||
// changes.
|
||||
let most = max_lines(ui.ctx().content_rect().height(), row_h);
|
||||
let lines = if app.ui.formula_bar_expanded { app.ui.formula_bar_lines.clamp(1, most) } else { last.lines.clamp(1, MAX_LINES) };
|
||||
let target = one_line(row_h) + (lines - 1) as f32 * row_h;
|
||||
let h = ui.ctx().animate_value_with_time(egui::Id::unique("formula_bar.height"), target, EASE_SECONDS);
|
||||
let mut layout = BarLayout { height: h, ..Default::default() };
|
||||
egui::Panel::top("formula_bar")
|
||||
let panel = egui::Panel::top("formula_bar")
|
||||
.exact_size(h)
|
||||
.frame(egui::Frame::NONE.fill(t.window).inner_margin(egui::Margin { left: 8, right: 8, top: 3, bottom: 3 }))
|
||||
.show(ui, |ui| {
|
||||
@@ -156,9 +174,58 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
});
|
||||
});
|
||||
layout.handle = resize_handle(app, ui, panel.response.rect, row_h, most);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(BarLayout::id(), layout));
|
||||
}
|
||||
|
||||
/// The most lines the bar takes in a window `window_h` tall: half the window, as in Excel, and
|
||||
/// never fewer than the chevron's [`MAX_LINES`].
|
||||
fn max_lines(window_h: f32, row_h: f32) -> usize {
|
||||
let fit = ((window_h * 0.5 - one_line(row_h)) / row_h).floor();
|
||||
if fit.is_finite() && fit > 0.0 { (fit as usize + 1).clamp(MAX_LINES, MAX_DRAG_LINES) } else { MAX_LINES }
|
||||
}
|
||||
|
||||
/// The lines a bar whose top is at `top` shows with its bottom edge dragged to `y`, in whole lines.
|
||||
fn dragged_lines(y: f32, top: f32, row_h: f32, most: usize) -> usize {
|
||||
let extra = ((y - top - one_line(row_h)) / row_h).round();
|
||||
if extra.is_finite() && extra > 0.0 { (extra as usize + 1).min(most) } else { 1 }
|
||||
}
|
||||
|
||||
/// The bar's bottom edge: dragging it sets the bar's height in whole lines, as in Excel. The bar
|
||||
/// follows the pointer; the release runs `view.expandFormulaBar` with the lines, so one drag is
|
||||
/// one step (one line collapses the bar).
|
||||
fn resize_handle(app: &mut SheetApp, ui: &mut egui::Ui, bar: Rect, row_h: f32, most: usize) -> Rect {
|
||||
let handle = Rect::from_x_y_ranges(bar.x_range(), (bar.bottom() - HANDLE)..=bar.bottom());
|
||||
let resp = ui.interact(handle, egui::Id::unique("formula_bar.resize"), Sense::drag());
|
||||
let name = app.ui.language.tr("Resize formula bar");
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, name));
|
||||
if resp.hovered() || resp.dragged() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
|
||||
}
|
||||
// The state before the drag: the preview changes it, the release puts it back for the command.
|
||||
let before = resp.id.with("before");
|
||||
if resp.drag_started() {
|
||||
let state = (app.ui.formula_bar_expanded, app.ui.formula_bar_lines);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(before, state));
|
||||
}
|
||||
if let Some(p) = resp.interact_pointer_pos().filter(|_| resp.dragged() || resp.drag_stopped()) {
|
||||
let lines = dragged_lines(p.y, bar.top(), row_h, most);
|
||||
if resp.drag_stopped() {
|
||||
if let Some((expanded, kept)) = ui.ctx().data_mut(|d| d.remove_temp::<(bool, usize)>(before)) {
|
||||
(app.ui.formula_bar_expanded, app.ui.formula_bar_lines) = (expanded, kept);
|
||||
}
|
||||
app.run_or_alert("view.expandFormulaBar", json!({"lines": lines}));
|
||||
} else {
|
||||
app.ui.formula_bar_expanded = lines > 1;
|
||||
if lines > 1 {
|
||||
app.ui.formula_bar_lines = lines;
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
/// Resolve ownership on every frame; never cache a spill range in presentation state.
|
||||
pub(crate) fn active_spill_anchor(app: &SheetApp) -> Option<gridcraft_engine::core::CellRef> {
|
||||
let d = app.session.active()?;
|
||||
|
||||
@@ -88,6 +88,9 @@ pub struct UiState {
|
||||
pub ribbon_tab: String,
|
||||
pub ribbon_collapsed: bool,
|
||||
pub formula_bar_expanded: bool,
|
||||
/// Lines the expanded formula bar shows: the height its bottom edge was last dragged to.
|
||||
/// Expanding it again (the chevron, Ctrl+Shift+U) returns to that height.
|
||||
pub formula_bar_lines: usize,
|
||||
pub status_bar: bool,
|
||||
/// Motion in the UI: the selection outline glides to a new selection (View ▸ Animations).
|
||||
pub animations: bool,
|
||||
@@ -137,6 +140,7 @@ impl Default for UiState {
|
||||
ribbon_tab: "Home".into(),
|
||||
ribbon_collapsed: false,
|
||||
formula_bar_expanded: false,
|
||||
formula_bar_lines: formula_bar::MAX_LINES,
|
||||
status_bar: true,
|
||||
animations: true,
|
||||
autocomplete: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Ctrl+Shift+U (Control+Shift+U in Excel for Mac) expands and collapses the formula bar.
|
||||
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use egui::{Event, Key, Modifiers, vec2};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use gridcraft_ui_egui::keymap::{Shortcut, lookup};
|
||||
@@ -84,3 +84,66 @@ fn expand_formula_bar_command_sets_or_toggles_the_state() {
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({})), Ok(json!({"on": false})));
|
||||
assert!(!expanded(&h));
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness, from: egui::Pos2, to: egui::Pos2) {
|
||||
use egui::PointerButton;
|
||||
h.input_mut().events.extend([
|
||||
Event::PointerMoved(from),
|
||||
Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
|
||||
]);
|
||||
h.step();
|
||||
for i in 1..=6 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 6.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(20);
|
||||
}
|
||||
|
||||
fn bar(h: &Harness) -> gridcraft_ui_egui::formula_bar::BarLayout {
|
||||
gridcraft_ui_egui::formula_bar::BarLayout::get(&h.ctx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_the_bottom_edge_sets_the_height_that_expanding_returns_to() {
|
||||
let mut h = harness();
|
||||
h.run_steps(20);
|
||||
let start = bar(&h);
|
||||
assert_eq!(start.visible_lines, 1, "one line of text shows a one-line bar");
|
||||
assert!(
|
||||
start.handle.height() > 0.0 && (start.handle.bottom() - start.field.bottom()).abs() <= 4.0,
|
||||
"the handle is the bar's bottom edge: {start:?}"
|
||||
);
|
||||
// Drag the edge down about five and a half rows: the bar snaps to whole lines.
|
||||
let row = 17.0;
|
||||
drag(&mut h, start.handle.center(), start.handle.center() + vec2(0.0, 5.5 * row));
|
||||
let lines = h.state().ui.formula_bar_lines;
|
||||
assert!(expanded(&h), "a taller bar is expanded");
|
||||
assert!((6..=7).contains(&lines), "about six more lines: {lines}");
|
||||
assert_eq!(bar(&h).visible_lines, lines, "the field shows the dragged lines: {:?}", bar(&h));
|
||||
// Collapse, then expand: back to the dragged height, not the default five lines.
|
||||
ctrl_shift_u(&mut h, CTRL_SHIFT);
|
||||
h.run_steps(20);
|
||||
assert!(!expanded(&h));
|
||||
assert_eq!(bar(&h).visible_lines, 1);
|
||||
ctrl_shift_u(&mut h, CTRL_SHIFT);
|
||||
h.run_steps(20);
|
||||
assert_eq!(bar(&h).visible_lines, lines, "expanding returns to the dragged height");
|
||||
// Dragging the edge up to one line collapses the bar and keeps the expanded height.
|
||||
let edge = bar(&h).handle.center();
|
||||
drag(&mut h, edge, edge - vec2(0.0, 20.0 * row));
|
||||
assert!(!expanded(&h), "a one-line bar is collapsed");
|
||||
assert_eq!(bar(&h).visible_lines, 1);
|
||||
assert_eq!(h.state().ui.formula_bar_lines, lines);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_formula_bar_with_lines_sets_the_expanded_height() {
|
||||
let mut h = harness();
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({"lines": 8})), Ok(json!({"on": true, "lines": 8})));
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({"lines": 1})), Ok(json!({"on": false, "lines": 8})), "one line collapses");
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({})), Ok(json!({"on": true})));
|
||||
assert_eq!(h.state().ui.formula_bar_lines, 8, "expanding returns to the set height");
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({"lines": 0})), Ok(json!({"on": false, "lines": 8})), "zero lines is one line");
|
||||
assert_eq!(h.state_mut().run("view.expandFormulaBar", json!({"lines": u64::MAX})), Ok(json!({"on": true, "lines": 100})), "capped");
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable tabs) | done; missing contextual tabs for pictures, shapes, sparklines, header/footer, slicers |
|
||||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing |
|
||||
| Key tips (Alt / F10 then letters) | yes, every ribbon control | **initial** (#43, 10-10): Alt or F10 shows key tips on Home for alignment, Paste Special and AutoFit plus legacy Alt sequences; other tabs and controls not yet (#33, #49) | 3–5 h |
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes; grows with its content from one to five lines (Alt+Enter, wrapped text, or viewing a multi-line cell), then scrolls with the caret in view, the Name Box and buttons staying on the first line; the chevron pins it at five; the in-cell editor shows every line | done; no drag-to-resize handle |
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes; grows with its content from one to five lines (Alt+Enter, wrapped text, or viewing a multi-line cell), then scrolls with the caret in view, the Name Box and buttons staying on the first line; dragging its bottom edge sets the height in whole lines (up to half the window; one line collapses it), and the chevron or Ctrl+Shift+U expands it back to that height (five lines until it is dragged); the in-cell editor shows every line | done; the Name Box width is fixed |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | Ctrl-drag (Cmd or Option on macOS) drops a copy (#971); active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes | done; dynamic-array spill children show the source formula muted and read-only, using current spill ownership |
|
||||
| Python cells: Python mode | yes | yes (11-01): Ctrl+Alt+Shift+P (Cmd+Option+Shift+P on a Mac), `=PY` then Tab, or editing a Python cell edits the code in the formula bar's code editor, with a PY badge beside fx; Enter starts a new line with the indentation, Tab and Shift+Tab indent, Ctrl+Enter (Cmd+Enter) puts the code in the cell through `python.set`, Esc cancels. Deleting all the code and committing clears a Python cell, as an empty entry clears any other cell. The formula bar shows while a Python edit is open, also when View › Formula Bar hides it. On a protected sheet, a Python cell formatted Hidden shows no code in the formula bar and starts no edit, as a hidden formula shows none | this build stores Python code and doesn't run it: new or changed code shows `#BLOCKED!`; no `xl()` pointing on the sheet yet |
|
||||
@@ -237,6 +237,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | minor | The formula bar's bottom edge is a resize handle: dragging it sets the bar's height in whole lines, up to half the window, and dragging it to one line collapses it; expanding (the chevron, Ctrl+Shift+U) returns to the dragged height, which is kept with the UI preferences. `view.expandFormulaBar {lines}` sets it (#1246) |
|
||||
| 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into |
|
||||
| 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused |
|
||||
| 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it |
|
||||
|
||||
Reference in new issue
Block a user