Skip to content

Commit 076ce15

Browse files
committed
Open Settings over the whole screen, dimming the dock with it
Opening Settings while the orchestrator dock is up laid the dialog into the chrome region right of the dock and dimmed only that region: the modal was squeezed into the remaining columns, and the dock stayed at full brightness beside it — reading as live even though the modal had already swallowed every key and click bound for it (`dispatch_modal_mouse` routes the whole mouse channel to Settings while it is open). That confinement existed only because the dock painted *after* the modals, so a full-frame modal would have had its left edge overpainted. Move the full-screen modals (settings, keybinding editor, event-debug dialog, calibration wizard) into `render_panels_and_modals`, drawn right after the dock and handed the whole frame — the same treatment the workspace-trust prompt already gets. They now centre in the full window and their dim pass covers the dock, the menu bar and the status bar. Painting the modals that late would have left them out of the terminal colour-capability conversion, so that pass moves to the very end of `render`, after every layer. That also fixes the dock itself, which has been emitting truecolor SGR on 256/16-colour terminals. Verified interactively in tmux (dock open + Settings, with and without the dock, narrow and tiny terminals, 256-colour mode) and covered by an e2e test that fails without the change.
1 parent f1757ea commit 076ce15

5 files changed

Lines changed: 121 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Keyboard and mouse input is now parsed by our own `fresh-input-parser` crate ins
3535
* Dock rows are fully clickable in compact (list) view, ordered by recency, and auto-name themselves from their terminal.
3636
* Fixed a crash when navigating to an unreachable remote workspace.
3737
* Searching the dock and then opening one of the matches no longer wipes the search — the filtered list is still there when you come back for the next one. Leaving the dock (`Esc`, clicking the editor) still clears it.
38+
* **Settings opens over the dock**, not beside it - the Settings dialog (and the keybinding editor) now centres on the whole screen and dims the dock along with everything else, instead of being squeezed into the columns right of the dock and leaving it bright, as if it were still live.
3839
* **Tidier workspace cards** - a card is now two rows instead of three: name with its git summary flush right, then the branch with the PR badge flush right. The branch starts at the card's left edge rather than floating mid-row, the empty third row is gone, and a branch that just repeats the workspace name gives its place to the project.
3940
* **Terminal**
4041
* Scrollback no longer loses output or gets stuck mid-scroll (#2649, reported by @dmknght).

crates/fresh-editor/src/app/render.rs

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -915,17 +915,13 @@ impl Editor {
915915
// Update menu context with current editor state
916916
self.update_menu_context();
917917

918-
// Settings / calibration-wizard / keybinding-editor / event-debug
919-
// modals, dimming the chrome behind each. Rendered before the menu
920-
// bar so open menus overlay them.
921-
self.render_modal_overlays(frame, chrome_area);
922-
923-
// The workspace-trust prompt is a blocking, top-most security modal.
924-
// It dims the *entire* frame (the dock included) and centres in the
925-
// full window, so it is rendered at the very end of this method —
926-
// after the dock and floating panels — rather than here, where the
927-
// dock's later pass would overpaint its left edge. See the bottom of
928-
// `render`.
918+
// The full-screen modals (settings, calibration wizard, keybinding
919+
// editor, event-debug dialog) and the blocking workspace-trust prompt
920+
// each dim the *entire* frame — the dock included — and centre in the
921+
// full window, so they are rendered at the very end of this method,
922+
// after the dock and floating panels, rather than here, where the
923+
// dock's later pass would overpaint their left edge. See the bottom of
924+
// `render` and `render_panels_and_modals`.
929925

930926
// Menu bar, drawn last so its dropdowns sit above all other content.
931927
self.render_menu_bar(frame, menu_bar_area);
@@ -964,19 +960,15 @@ impl Editor {
964960
}
965961
}
966962

967-
// Convert all colors for terminal capability (256/16 color fallback)
968-
crate::view::color_support::convert_buffer_colors(
969-
frame.buffer_mut(),
970-
self.color_capability,
971-
);
972-
973-
// Frame-buffer animations run last so they mutate the final paint.
963+
// Frame-buffer animations run after the main draw so they mutate the
964+
// final paint.
974965
self.active_window_mut()
975966
.animations
976967
.apply_all(frame.buffer_mut());
977968

978-
// Dock, floating panel, theme-info popup, and the workspace-trust
979-
// modal — the topmost layers, drawn above prompts/popups/animations.
969+
// Dock, full-screen modals, floating panel, theme-info popup, and the
970+
// workspace-trust modal — the topmost layers, drawn above
971+
// prompts/popups/animations.
980972
self.render_panels_and_modals(
981973
frame,
982974
size,
@@ -985,6 +977,15 @@ impl Editor {
985977
top_is_trust_modal,
986978
&theme_clone,
987979
);
980+
981+
// Convert all colors for terminal capability (256/16 color fallback).
982+
// Dead last, so the layers painted above — dock, full-screen modals,
983+
// animations — go through the fallback too instead of emitting
984+
// truecolor SGR on a terminal that cannot render it.
985+
crate::view::color_support::convert_buffer_colors(
986+
frame.buffer_mut(),
987+
self.color_capability,
988+
);
988989
}
989990

990991
/// Render the search-options bar into `area` when `show_search_options`
@@ -1318,8 +1319,9 @@ impl Editor {
13181319
}
13191320

13201321
/// Render the topmost layers: the dock and floating widget panel (each in
1321-
/// its own slot), the theme-info popup, and the blocking workspace-trust
1322-
/// modal. Drawn after every other layer so they sit on top.
1322+
/// its own slot), the full-screen modals (settings, keybinding editor,
1323+
/// …), the theme-info popup, and the blocking workspace-trust modal.
1324+
/// Drawn after every other layer so they sit on top.
13231325
fn render_panels_and_modals(
13241326
&mut self,
13251327
frame: &mut Frame,
@@ -1340,6 +1342,14 @@ impl Editor {
13401342
}
13411343
}
13421344

1345+
// Settings / calibration-wizard / keybinding-editor / event-debug —
1346+
// full-screen modals. They get the whole frame (`size`), not the
1347+
// chrome region right of the dock: each dims everything behind it,
1348+
// the dock included, and centres in the full window. Drawn here,
1349+
// after the dock's own pass, so the dock cannot overpaint the
1350+
// modal's left edge.
1351+
self.render_modal_overlays(frame, size);
1352+
13431353
// The theme-info popup (Ctrl+Right-Click) anchors to an absolute
13441354
// screen cell that may sit over the dock column, so draw it after
13451355
// the dock — otherwise the dock paints over it and its "Open in
@@ -1953,12 +1963,16 @@ impl Editor {
19531963
}
19541964
}
19551965

1956-
/// Render the modal overlays that dim the chrome behind them: settings,
1966+
/// Render the modal overlays that dim everything behind them: settings,
19571967
/// calibration wizard, keybinding editor, and event-debug dialog. Each is
19581968
/// drawn only for the TUI (`!suppress_chrome_cells`); the web projects
1959-
/// them natively. Rendered before the menu bar so open menus overlay them.
1960-
fn render_modal_overlays(&mut self, frame: &mut Frame, chrome_area: ratatui::layout::Rect) {
1961-
// Render settings modal (before menu bar so menus can overlay)
1969+
/// them natively.
1970+
///
1971+
/// `area` is the whole frame — these are full-screen modals, so the dim
1972+
/// pass covers the dock column too and each dialog centres in the full
1973+
/// window. They are called from `render_panels_and_modals` (after the
1974+
/// dock paints) so the dock cannot overpaint them.
1975+
fn render_modal_overlays(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
19621976
// Check visibility first to avoid borrow conflict with dimming
19631977
// The web renders Settings natively from `settings_view`; paint cells
19641978
// only for the TUI.
@@ -1970,10 +1984,12 @@ impl Editor {
19701984
.map(|s| s.visible)
19711985
.unwrap_or(false);
19721986
if settings_visible {
1973-
// Dim the editor content behind the settings modal. Use the
1974-
// chrome area (right of a left dock) so the modal sits beside
1975-
// the persistent dock instead of being overlapped by it.
1976-
crate::view::dimming::apply_dimming(frame, chrome_area);
1987+
// Dim everything behind the settings modal — the editor chrome
1988+
// *and* the dock. The dock is input-inaccessible while the modal
1989+
// is up (`dispatch_modal_mouse` routes every click to settings),
1990+
// so leaving it at full brightness read as if it were still live
1991+
// beside a dialog that had already swallowed its input.
1992+
crate::view::dimming::apply_dimming(frame, area);
19771993
}
19781994
if let Some(ref mut settings_state) = self.settings_state {
19791995
if !draw_settings {
@@ -1988,7 +2004,7 @@ impl Editor {
19882004
settings_state.update_focus_states();
19892005
let settings_layout = crate::view::settings::render_settings(
19902006
frame,
1991-
chrome_area,
2007+
area,
19922008
settings_state,
19932009
&self.theme.read().unwrap(),
19942010
);
@@ -2001,10 +2017,10 @@ impl Editor {
20012017
if !self.suppress_chrome_cells {
20022018
if let Some(ref wizard) = self.calibration_wizard {
20032019
// Dim the editor content behind the wizard modal
2004-
crate::view::dimming::apply_dimming(frame, chrome_area);
2020+
crate::view::dimming::apply_dimming(frame, area);
20052021
crate::view::calibration_wizard::render_calibration_wizard(
20062022
frame,
2007-
chrome_area,
2023+
area,
20082024
wizard,
20092025
&self.theme.read().unwrap(),
20102026
);
@@ -2019,10 +2035,10 @@ impl Editor {
20192035
// paint cells only for the TUI.
20202036
if draw_aux {
20212037
if let Some(ref mut kb_editor) = self.keybinding_editor {
2022-
crate::view::dimming::apply_dimming(frame, chrome_area);
2038+
crate::view::dimming::apply_dimming(frame, area);
20232039
crate::view::keybinding_editor::render_keybinding_editor(
20242040
frame,
2025-
chrome_area,
2041+
area,
20262042
kb_editor,
20272043
&self.theme.read().unwrap(),
20282044
);
@@ -2033,10 +2049,10 @@ impl Editor {
20332049
if draw_aux {
20342050
if let Some(ref debug) = self.active_window().event_debug {
20352051
// Dim the editor content behind the dialog modal
2036-
crate::view::dimming::apply_dimming(frame, chrome_area);
2052+
crate::view::dimming::apply_dimming(frame, area);
20372053
crate::view::event_debug::render_event_debug(
20382054
frame,
2039-
chrome_area,
2055+
area,
20402056
debug,
20412057
&self.theme.read().unwrap(),
20422058
);

crates/fresh-editor/src/view/settings/render.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ pub fn render_settings(
8989
// Calculate modal size (90% of screen width, 90% height to fill most of available space)
9090
let modal_width = (area.width * 90 / 100).min(160);
9191
let modal_height = area.height * 90 / 100;
92-
// Offsets must be ABSOLUTE — `area.x` / `area.y` are nonzero when
93-
// `area` is the chrome region right of the dock (or a bottom-anchored
94-
// split). Centring with bare `area.width / 2` placed the modal at the
95-
// FRAME origin, where the dock then over-drew its left edge — hiding
96-
// the title bar and clipping the rounded top-left corner.
92+
// Offsets must be ABSOLUTE — `area.x` / `area.y` are not assumed to be
93+
// zero (this is the full frame today, but the modal must centre in
94+
// whatever rect it is handed). Centring with bare `area.width / 2` placed
95+
// the modal at the FRAME origin, where the dock then over-drew its left
96+
// edge — hiding the title bar and clipping the rounded top-left corner.
9797
let modal_x = area.x + (area.width.saturating_sub(modal_width)) / 2;
9898
let modal_y = area.y + (area.height.saturating_sub(modal_height)) / 2;
9999

crates/fresh-editor/tests/e2e/orchestrator_dock.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3614,3 +3614,62 @@ fn dock_card_leaves_the_second_row_empty_rather_than_echo_the_name() {
36143614
"the card still ends after its second row, got {after:?}"
36153615
);
36163616
}
3617+
3618+
/// The Settings modal is a *full-screen* modal: with the dock open it must
3619+
/// centre in the whole window — painting over the dock column — and dim the
3620+
/// dock along with the rest of the frame.
3621+
///
3622+
/// It used to be laid into the chrome region right of the dock (the dock's
3623+
/// later paint pass would otherwise overpaint the modal's left edge), which
3624+
/// squeezed the dialog into the remaining columns and left the dock at full
3625+
/// brightness beside it, reading as live even though the modal had already
3626+
/// swallowed every key and click bound for it.
3627+
#[test]
3628+
fn settings_modal_covers_the_full_screen_and_dims_the_dock() {
3629+
let (_tmp, root) = setup_project("alphaproj");
3630+
let mut h =
3631+
EditorTestHarness::with_config_and_working_dir(120, 32, Default::default(), root.clone())
3632+
.unwrap();
3633+
h.render().unwrap();
3634+
open_dock(&mut h);
3635+
3636+
// The dock's right border on the toolbar row (row 0) marks the width of
3637+
// its column; row 0 sits above the modal, so it survives it.
3638+
let cols = h.screen_row_text(0).chars().count() as u16;
3639+
let dock_border_col = (0..cols)
3640+
.find(|&c| h.get_cell(c, 0).as_deref() == Some("│"))
3641+
.expect("dock right border (│) should be present on the toolbar row");
3642+
3643+
// Sample a dock cell before the modal opens: its title, which the
3644+
// modal does not cover.
3645+
let (title_col, title_row) = h
3646+
.find_text_on_screen("Orchestrator")
3647+
.expect("dock title should be on screen");
3648+
let undimmed_fg = h.get_cell_style(title_col, title_row).unwrap().fg;
3649+
3650+
h.open_settings().unwrap();
3651+
3652+
// The modal's top-left corner lands *inside* the dock column, i.e. it
3653+
// is centred on the full frame rather than on the chrome beside the
3654+
// dock.
3655+
let (modal_left, modal_row) = h
3656+
.find_text_on_screen("Settings [")
3657+
.expect("settings modal title should be on screen");
3658+
let corner_col = (0..modal_left)
3659+
.rev()
3660+
.find(|&c| h.get_cell(c, modal_row).as_deref() == Some("╭"))
3661+
.expect("settings modal should draw its rounded top-left corner");
3662+
assert!(
3663+
corner_col < dock_border_col,
3664+
"the settings modal must start inside the dock column (corner at {corner_col}, \
3665+
dock border at {dock_border_col}):\n{}",
3666+
h.screen_to_string()
3667+
);
3668+
3669+
// ...and the dock is dimmed behind it rather than left looking live.
3670+
let dimmed_fg = h.get_cell_style(title_col, title_row).unwrap().fg;
3671+
assert_ne!(
3672+
undimmed_fg, dimmed_fg,
3673+
"the dock must dim while the settings modal is up"
3674+
);
3675+
}

docs/internal/rendering-and-layout.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ Fresh is an **immediate-mode** TUI: there is no retained widget tree and no dirt
1919
6. Carve the file-explorer sidebar out of the main content area and paint it.
2020
7. Fire `lines_changed` plugin hooks for newly-visible lines (lets plugins add overlays *before* the content render).
2121
8. **Render split content**: a single split-borrow of the active window yields the buffers, the split manager, and the view states, and the split renderer paints every visible leaf into the frame buffer. It returns per-leaf layout caches (split areas, tab layouts, view-line mappings, scrollbar areas, …) stored on the window for the next frame's hit-testing.
22-
9. Post-content passes: cursor-jump animation, viewport-change hooks, popups, modals, menu bar, status bar, context menus, software cursor, dock/overlay painting, then dimming behind modals.
22+
9. Post-content passes: cursor-jump animation, viewport-change hooks, popups, menu bar, status bar, context menus, software cursor, frame-buffer animations.
23+
10. Topmost layers, painted after everything above: the dock into its carved column; then the full-screen modals (settings, keybinding editor, event-debug dialog, calibration wizard), each dimming the *whole* frame behind it — the dock included — and centring in the full window rather than in the chrome beside the dock; then the floating widget panel, the theme-info popup, and the blocking workspace-trust prompt. Modals paint *after* the dock so the dock's own pass cannot overpaint their left edge.
24+
11. Terminal colour-capability conversion (256/16 fallback) over the finished buffer, so every layer — including the dock, the modals, and animation output — goes through it.
2325

2426
### `RenderStyle`, `EditorRenderConfig`, and rendering into an arbitrary buffer
2527

0 commit comments

Comments
 (0)