#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:
Hector VillarrealandClaude Opus 5.5 committed 2026-11-03 02:41:57 -06:00
1 parent 178f3eb745
commit dea1359c7e
6 files changed
+160 -10

No files matched your search

+13 -2
View File
@@ -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 };
+5 -1
View File
@@ -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", ""),
+72 -5
View File
@@ -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()?;
+4
View File
@@ -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,
+64 -1
View File
@@ -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
View File
@@ -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 |