Take the window arithmetic out of the window

main.rs is 3,884 lines and 56% of the application, so everything inside it is
as expensive to read as the renderer around it. The first thing to leave is
the part that never needed a renderer at all: how tall the workspace is once
the header, tab bar and footer are subtracted, how many panes fit beside the
rail, which slice of the attached panes is on screen, and how many rows and
columns of a measured cell that leaves.

This arithmetic is the contract with the PTY -- a program lays itself out from
the columns it is told it has, so an error of one column here is a wrapped
line in vim and a broken table in git log. That makes it exactly the code that
should be tested against numbers rather than against a window, and decision
0009's pane capacities were already wrong once because they were quoted from a
guessed cell width.

CellMetrics::measure stays behind. Asking the text system for a glyph advance
needs a live App, so the measurement remains a renderer's job and only the
answer crosses over, as two plain f32s. Size<Pixels> stops crossing at all:
geometry works in a local WindowSize and main.rs converts on the way in
through one From impl, which is the entire boundary. The module imports no
gpui, matching sidebar::model, so its two tests run in the headless job.

One thing removed rather than moved: lossless_f32 carried an
allow(clippy::unreadable_literal) with the reason "six-digit colour hex reads
whole". It had drifted up from the terminal colour tables below it and applied
to a function containing no colour and no literal it could suppress.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPYebLiN2w4TqnHUYGdECq
This commit is contained in:
Metal Agent
2026-09-01 12:55:59 -07:00
co-authored by Claude Opus 5
parent 9a29e8e335
commit 5564063aa5
2 changed files with 240 additions and 174 deletions
+217
View File
@@ -0,0 +1,217 @@
//! The arithmetic that turns a window into a terminal grid.
//!
//! Everything here is a pure function of numbers: how tall the workspace is
//! once the chrome is subtracted, how many panes fit beside the rail, which
//! slice of the attached panes is on screen, and how many rows and columns of
//! a measured cell that leaves. None of it needs a renderer, and that is the
//! reason it lives apart from one.
//!
//! It matters because this arithmetic is the contract with the PTY. A program
//! running under Lumbridge lays itself out from the columns it is told it has,
//! so an error of one column here is a wrapped line in `vim` and a broken table
//! in `git log`. Decision 0009 fixes the pane capacities, and the numbers it
//! quotes were once derived from a *guessed* cell width — the kind of mistake
//! that is invisible in a screenshot and obvious in a test. Keeping the
//! arithmetic gpui-free means those tests run in the fast headless CI job,
//! against real numbers, without opening a window.
//!
//! The one thing deliberately *not* here is [`CellMetrics::measure`]. Asking
//! the text system for a glyph advance needs a live `App`, so the constructor
//! stays in the view layer and hands the answer down as plain `f32`s. The
//! measurement is a renderer's job; what is done with it is not.
use lumbridge_terminal::TerminalDimensions;
const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0;
pub(crate) const APP_FOOTER_HEIGHT: f32 = 46.0;
const TERMINAL_CONTENT_VERTICAL_INSET: f32 = 24.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
const WORK_PANEL_GAP: f32 = 4.0;
/// Fallbacks, used only until the text system has been asked.
///
/// These were the whole story until now, and 8.4 was a guess: nothing had ever
/// measured the monospace face actually in use, so the column arithmetic and the
/// painted glyph advance were two independent opinions that happened to be
/// close. [`CellMetrics`] replaces them with a measurement.
pub(crate) const TERMINAL_CELL_WIDTH: f32 = 8.4;
pub(crate) const TERMINAL_CELL_HEIGHT: f32 = 18.0;
/// A window's size in logical pixels, with no renderer type attached.
///
/// The view layer holds a `gpui::Size<Pixels>` and converts on the way in. That
/// conversion is one line and it buys the whole module: the layout rules below
/// can be exercised by a test that names two numbers, rather than by a test
/// that needs a windowing system to exist.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct WindowSize {
pub(crate) width: f32,
pub(crate) height: f32,
}
/// The measured size of one terminal cell.
///
/// A terminal's whole geometry is columns × advance. Guessing the advance means
/// the last column is clipped or a gap is left, and it means the size handed to
/// the PTY describes a window that is not the one on screen.
#[derive(Clone, Copy, Debug)]
pub(crate) struct CellMetrics {
pub(crate) advance: f32,
pub(crate) line_height: f32,
}
impl Default for CellMetrics {
fn default() -> Self {
Self {
advance: TERMINAL_CELL_WIDTH,
line_height: TERMINAL_CELL_HEIGHT,
}
}
}
/// Widens a small count for layout arithmetic.
///
/// `f32` represents every integer below 2^24 exactly, and the values passed
/// here are panel and column counts. Saturating there rather than at `usize::MAX`
/// keeps the conversion exact for every value this can actually receive.
pub(crate) fn lossless_f32(value: usize) -> f32 {
const EXACT_LIMIT: usize = 1 << 24;
#[allow(
clippy::cast_precision_loss,
reason = "clamped below 2^24, where f32 is exact"
)]
{
value.min(EXACT_LIMIT) as f32
}
}
/// Narrows an already-clamped dimension.
///
/// Every caller clamps into `2.0..=u16::MAX` first, so this is a narrowing of a
/// value known to fit — but `as` would silently produce garbage if a caller ever
/// stopped clamping, and a NaN would become zero. This saturates instead, which
/// is why the `with_cell_size` expect below is honest rather than hopeful.
pub(crate) fn clamp_to_u16(value: f32) -> u16 {
if value.is_nan() {
return 1;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "clamped into u16 range on the line above"
)]
{
value.clamp(0.0, f32::from(u16::MAX)) as u16
}
}
pub(crate) fn terminal_dimensions_for_window(
window_size: WindowSize,
attached_panel_count: usize,
sidebar_width: f32,
cell: CellMetrics,
) -> TerminalDimensions {
let width = window_size.width;
let height = window_size.height;
let panel_count = visible_panel_count(window_size, sidebar_width)
.min(attached_panel_count)
.max(1);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
let workspace_width = (width - sidebar_width).max(0.0);
// A panel count is single digits; the precision lint is about 2^24 and up.
let panel_gaps = WORK_PANEL_GAP * lossless_f32(panel_count.saturating_sub(1));
let panel_width =
((workspace_width - panel_gaps).max(0.0) / lossless_f32(panel_count)).max(0.0);
let terminal_width = (panel_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / cell.line_height)
.floor()
.clamp(2.0, f32::from(u16::MAX));
let columns = (terminal_width / cell.advance)
.floor()
.clamp(20.0, f32::from(u16::MAX));
TerminalDimensions::with_cell_size(
clamp_to_u16(rows),
clamp_to_u16(columns),
clamp_to_u16(cell.advance.round()),
clamp_to_u16(cell.line_height.round()),
)
.expect("geometry clamps terminal dimensions above zero")
}
/// How many panes fit beside the rail.
///
/// Measured *after* the sidebar, as decision 0009 specifies, which is why
/// widening the rail can drop the workspace from three panes to one. That is
/// correct and it is visible while dragging.
pub(crate) fn visible_panel_count(window_size: WindowSize, sidebar_width: f32) -> usize {
let workspace_width = (window_size.width - sidebar_width).max(0.0);
if workspace_width >= 2_800.0 {
5
} else if workspace_width >= 1_100.0 {
3
} else {
1
}
}
pub(crate) fn visible_panel_range(
total: usize,
selected: usize,
panel_count: usize,
) -> std::ops::Range<usize> {
let count = panel_count.clamp(1, total.max(1)).min(total);
let start = selected
.saturating_sub(count / 2)
.min(total.saturating_sub(count));
start..start + count
}
#[cfg(test)]
mod tests {
use super::{
CellMetrics, WindowSize, terminal_dimensions_for_window, visible_panel_count,
visible_panel_range,
};
use crate::sidebar::model::DEFAULT_WIDTH;
#[test]
fn terminal_geometry_tracks_middle_sixty_percent_per_panel() {
let dimensions = terminal_dimensions_for_window(
WindowSize {
width: 1500.0,
height: 960.0,
},
5,
248.0,
CellMetrics::default(),
);
assert_eq!(dimensions.rows(), 26);
assert_eq!(dimensions.columns(), 45);
let ultrawide = WindowSize {
width: 3440.0,
height: 1440.0,
};
assert_eq!(visible_panel_count(ultrawide, DEFAULT_WIDTH), 5);
let dimensions =
terminal_dimensions_for_window(ultrawide, 5, DEFAULT_WIDTH, CellMetrics::default());
assert_eq!(dimensions.rows(), 42);
assert_eq!(dimensions.columns(), 71);
let two_panels =
terminal_dimensions_for_window(ultrawide, 2, DEFAULT_WIDTH, CellMetrics::default());
assert_eq!(two_panels.rows(), 42);
assert_eq!(two_panels.columns(), 185);
}
#[test]
fn panel_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_panel_range(6, 0, 5), 0..5);
assert_eq!(visible_panel_range(6, 5, 5), 1..6);
assert_eq!(visible_panel_range(6, 3, 3), 2..5);
assert_eq!(visible_panel_range(6, 4, 1), 4..5);
}
}
+23 -174
View File
@@ -1,4 +1,5 @@
mod attention; mod attention;
mod geometry;
mod keymap; mod keymap;
mod panel_registry; mod panel_registry;
mod settings_view; mod settings_view;
@@ -31,6 +32,10 @@ use lumbridge_ui_fixture::{
}; };
use attention::{Attention, AttentionKind, AttentionSignal, AttentionSource}; use attention::{Attention, AttentionKind, AttentionSignal, AttentionSource};
use geometry::{
APP_FOOTER_HEIGHT, CellMetrics, TERMINAL_CELL_HEIGHT, TERMINAL_CELL_WIDTH, WindowSize,
clamp_to_u16, terminal_dimensions_for_window, visible_panel_count, visible_panel_range,
};
use lumbridge_harness::MonotonicWallClock; use lumbridge_harness::MonotonicWallClock;
use lumbridge_settings::{Page, ProcessEnv, Settings, SettingsContent}; use lumbridge_settings::{Page, ProcessEnv, Settings, SettingsContent};
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane}; use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
@@ -45,19 +50,6 @@ const TIMING_SAMPLE_LIMIT: usize = 256;
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16); const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
const RUNTIME_DRAIN_LIMIT: usize = 64; const RUNTIME_DRAIN_LIMIT: usize = 64;
const WORKSPACE_SNAPSHOT_ID: &str = "lumbridge-code-gpui-spike"; const WORKSPACE_SNAPSHOT_ID: &str = "lumbridge-code-gpui-spike";
const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0;
const APP_FOOTER_HEIGHT: f32 = 46.0;
const TERMINAL_CONTENT_VERTICAL_INSET: f32 = 24.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
/// Fallbacks, used only until the text system has been asked.
///
/// These were the whole story until now, and 8.4 was a guess: nothing had ever
/// measured the monospace face actually in use, so the column arithmetic and the
/// painted glyph advance were two independent opinions that happened to be
/// close. [`CellMetrics`] replaces them with a measurement.
const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
/// The point size the terminal is painted at. /// The point size the terminal is painted at.
const TERMINAL_FONT_SIZE: f32 = 13.0; const TERMINAL_FONT_SIZE: f32 = 13.0;
/// Named rather than "monospace" so the measurement and the painting agree on a /// Named rather than "monospace" so the measurement and the painting agree on a
@@ -65,22 +57,18 @@ const TERMINAL_FONT_SIZE: f32 = 13.0;
/// and the renderer pick another. /// and the renderer pick another.
const TERMINAL_FONT_FAMILY: &str = "monospace"; const TERMINAL_FONT_FAMILY: &str = "monospace";
/// The measured size of one terminal cell. /// Converts the renderer's window size into the plain numbers the layout
/// arithmetic works in.
/// ///
/// A terminal's whole geometry is columns × advance. Guessing the advance means /// This one line is the entire boundary between the view layer and
/// the last column is clipped or a gap is left, and it means the size handed to /// [`crate::geometry`]. Doing the conversion here rather than letting
/// the PTY describes a window that is not the one on screen. /// `Size<Pixels>` reach the arithmetic is what keeps the pane-capacity and
#[derive(Clone, Copy, Debug)] /// row/column rules testable without a windowing system.
struct CellMetrics { impl From<Size<Pixels>> for WindowSize {
advance: f32, fn from(size: Size<Pixels>) -> Self {
line_height: f32,
}
impl Default for CellMetrics {
fn default() -> Self {
Self { Self {
advance: TERMINAL_CELL_WIDTH, width: f32::from(size.width),
line_height: TERMINAL_CELL_HEIGHT, height: f32::from(size.height),
} }
} }
} }
@@ -118,7 +106,6 @@ impl CellMetrics {
} }
} }
} }
const WORK_PANEL_GAP: f32 = 4.0;
/// How close to the seam a press has to land to start a resize. /// How close to the seam a press has to land to start a resize.
const SIDEBAR_GRAB_RADIUS: f32 = 4.0; const SIDEBAR_GRAB_RADIUS: f32 = 4.0;
const TERMINAL_ROW_STEP: u16 = 2; const TERMINAL_ROW_STEP: u16 = 2;
@@ -434,46 +421,6 @@ fn terminal_paint_rows(
.collect() .collect()
} }
#[allow(
clippy::unreadable_literal,
reason = "six-digit colour hex reads whole"
)]
/// Widens a small count for layout arithmetic.
///
/// `f32` represents every integer below 2^24 exactly, and the values passed
/// here are panel and column counts. Saturating there rather than at `usize::MAX`
/// keeps the conversion exact for every value this can actually receive.
fn lossless_f32(value: usize) -> f32 {
const EXACT_LIMIT: usize = 1 << 24;
#[allow(
clippy::cast_precision_loss,
reason = "clamped below 2^24, where f32 is exact"
)]
{
value.min(EXACT_LIMIT) as f32
}
}
/// Narrows an already-clamped dimension.
///
/// Every caller clamps into `2.0..=u16::MAX` first, so this is a narrowing of a
/// value known to fit — but `as` would silently produce garbage if a caller ever
/// stopped clamping, and a NaN would become zero. This saturates instead, which
/// is why the `with_cell_size` expect below is honest rather than hopeful.
fn clamp_to_u16(value: f32) -> u16 {
if value.is_nan() {
return 1;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "clamped into u16 range on the line above"
)]
{
value.clamp(0.0, f32::from(u16::MAX)) as u16
}
}
#[allow( #[allow(
clippy::unreadable_literal, clippy::unreadable_literal,
reason = "six-digit colour hex reads whole" reason = "six-digit colour hex reads whole"
@@ -555,69 +502,6 @@ fn dim_color(color: Rgba) -> Rgba {
} }
} }
fn terminal_dimensions_for_window(
window_size: Size<Pixels>,
attached_panel_count: usize,
sidebar_width: f32,
cell: CellMetrics,
) -> TerminalDimensions {
let width = f32::from(window_size.width);
let height = f32::from(window_size.height);
let panel_count = visible_panel_count(window_size, sidebar_width)
.min(attached_panel_count)
.max(1);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
let workspace_width = (width - sidebar_width).max(0.0);
// A panel count is single digits; the precision lint is about 2^24 and up.
let panel_gaps = WORK_PANEL_GAP * lossless_f32(panel_count.saturating_sub(1));
let panel_width =
((workspace_width - panel_gaps).max(0.0) / lossless_f32(panel_count)).max(0.0);
let terminal_width = (panel_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / cell.line_height)
.floor()
.clamp(2.0, f32::from(u16::MAX));
let columns = (terminal_width / cell.advance)
.floor()
.clamp(20.0, f32::from(u16::MAX));
TerminalDimensions::with_cell_size(
clamp_to_u16(rows),
clamp_to_u16(columns),
clamp_to_u16(cell.advance.round()),
clamp_to_u16(cell.line_height.round()),
)
.expect("geometry clamps terminal dimensions above zero")
}
/// How many panes fit beside the rail.
///
/// Measured *after* the sidebar, as decision 0009 specifies, which is why
/// widening the rail can drop the workspace from three panes to one. That is
/// correct and it is visible while dragging.
fn visible_panel_count(window_size: Size<Pixels>, sidebar_width: f32) -> usize {
let workspace_width = (f32::from(window_size.width) - sidebar_width).max(0.0);
if workspace_width >= 2_800.0 {
5
} else if workspace_width >= 1_100.0 {
3
} else {
1
}
}
fn visible_panel_range(
total: usize,
selected: usize,
panel_count: usize,
) -> std::ops::Range<usize> {
let count = panel_count.clamp(1, total.max(1)).min(total);
let start = selected
.saturating_sub(count / 2)
.min(total.saturating_sub(count));
start..start + count
}
impl RenderTiming { impl RenderTiming {
fn mark_dispatch(&mut self) { fn mark_dispatch(&mut self) {
self.pending_dispatch = Some(Instant::now()); self.pending_dispatch = Some(Instant::now());
@@ -764,7 +648,7 @@ impl LumbridgeShell {
let sidebar = SidebarState::default(); let sidebar = SidebarState::default();
let cell = CellMetrics::measure(cx); let cell = CellMetrics::measure(cx);
let terminal_dimensions = terminal_dimensions_for_window( let terminal_dimensions = terminal_dimensions_for_window(
window.bounds().size, window.bounds().size.into(),
panels.attached_count(), panels.attached_count(),
sidebar.width(), sidebar.width(),
cell, cell,
@@ -787,7 +671,7 @@ impl LumbridgeShell {
cx.observe_window_bounds(window, |shell, window, cx| { cx.observe_window_bounds(window, |shell, window, cx| {
let dimensions = terminal_dimensions_for_window( let dimensions = terminal_dimensions_for_window(
window.bounds().size, window.bounds().size.into(),
shell.panels.attached_count(), shell.panels.attached_count(),
shell.sidebar.width(), shell.sidebar.width(),
shell.cell, shell.cell,
@@ -1127,7 +1011,7 @@ impl LumbridgeShell {
fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context<Self>) { fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context<Self>) {
let dimensions = terminal_dimensions_for_window( let dimensions = terminal_dimensions_for_window(
window.bounds().size, window.bounds().size.into(),
self.panels.attached_count(), self.panels.attached_count(),
self.sidebar.width(), self.sidebar.width(),
self.cell, self.cell,
@@ -1150,7 +1034,7 @@ impl LumbridgeShell {
let pane = self.panels.create(kind); let pane = self.panels.create(kind);
if kind == PanelKind::Terminal { if kind == PanelKind::Terminal {
let dimensions = terminal_dimensions_for_window( let dimensions = terminal_dimensions_for_window(
window.bounds().size, window.bounds().size.into(),
self.panels.attached_count(), self.panels.attached_count(),
self.sidebar.width(), self.sidebar.width(),
self.cell, self.cell,
@@ -1200,7 +1084,7 @@ impl LumbridgeShell {
return; return;
} }
let dimensions = terminal_dimensions_for_window( let dimensions = terminal_dimensions_for_window(
window.bounds().size, window.bounds().size.into(),
self.panels.attached_count(), self.panels.attached_count(),
self.sidebar.width(), self.sidebar.width(),
self.cell, self.cell,
@@ -3502,7 +3386,7 @@ fn provenance_chip(
impl Render for LumbridgeShell { impl Render for LumbridgeShell {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let panel_capacity = visible_panel_count(window.bounds().size, self.sidebar.width()); let panel_capacity = visible_panel_count(window.bounds().size.into(), self.sidebar.width());
let attached_panes = self.panels.attached_ids(); let attached_panes = self.panels.attached_ids();
let attached_count = attached_panes.len(); let attached_count = attached_panes.len();
let detached_entries = self let detached_entries = self
@@ -3743,14 +3627,11 @@ fn main() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::CellMetrics;
use super::{ use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll, KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts, indexed_terminal_color, terminal_key_from_parts, terminal_paint_rows,
terminal_paint_rows, terminal_scroll_from_parts, visible_panel_count, visible_panel_range, terminal_scroll_from_parts,
}; };
use crate::sidebar::model::DEFAULT_WIDTH;
use gpui::{px, size};
#[test] #[test]
fn maps_named_and_composed_gpui_keys_to_terminal_input() { fn maps_named_and_composed_gpui_keys_to_terminal_input() {
@@ -3802,38 +3683,6 @@ mod tests {
assert_eq!(control.key, TerminalKey::Text("c".into())); assert_eq!(control.key, TerminalKey::Text("c".into()));
} }
#[test]
fn terminal_geometry_tracks_middle_sixty_percent_per_panel() {
let dimensions = terminal_dimensions_for_window(
size(px(1500.0), px(960.0)),
5,
248.0,
CellMetrics::default(),
);
assert_eq!(dimensions.rows(), 26);
assert_eq!(dimensions.columns(), 45);
let ultrawide = size(px(3440.0), px(1440.0));
assert_eq!(visible_panel_count(ultrawide, DEFAULT_WIDTH), 5);
let dimensions =
terminal_dimensions_for_window(ultrawide, 5, DEFAULT_WIDTH, CellMetrics::default());
assert_eq!(dimensions.rows(), 42);
assert_eq!(dimensions.columns(), 71);
let two_panels =
terminal_dimensions_for_window(ultrawide, 2, DEFAULT_WIDTH, CellMetrics::default());
assert_eq!(two_panels.rows(), 42);
assert_eq!(two_panels.columns(), 185);
}
#[test]
fn panel_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_panel_range(6, 0, 5), 0..5);
assert_eq!(visible_panel_range(6, 5, 5), 1..6);
assert_eq!(visible_panel_range(6, 3, 3), 2..5);
assert_eq!(visible_panel_range(6, 4, 1), 4..5);
}
#[test] #[test]
fn shift_navigation_controls_scrollback_without_reaching_the_pty() { fn shift_navigation_controls_scrollback_without_reaching_the_pty() {
assert_eq!( assert_eq!(