diff --git a/CHANGELOG.md b/CHANGELOG.md index 08960b23..739658c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to tty7 are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- **The macOS traffic lights sit on the same line as the tiles beside them.** + The buttons AppKit draws are 14 points tall and gpui hangs the top of the + frame `y` points below the window's top edge, so the lights' centre falls + seven points under the bar's top; the New Tab and sidebar tiles are centred + in the bar's content box instead — its height less the hairline `TitleBar` + draws along the bottom edge. The position was still the one the 40-point bar + called for, carried over by hand when the bar grew to 48 without that + hairline in it, which left the lights half a point below everything else in + the row: one device pixel on a Retina panel. `traffic_light_position()` now + derives it from `TITLE_BAR_HEIGHT`, and a test holds the lights to the tile + line through the next change of bar height. + ## [26.9.4] - 2026-09-29 ### Added diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 1652df26..04bfb375 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -11,14 +11,33 @@ use crate::terminal::view::{ ClearScrollback, CopyText, CutText, FindInTerminal, FindNext, FindPrevious, PasteText, RedoEdit, SelectAll, UndoEdit, }; +use crate::ui::app::TITLE_BAR_HEIGHT; use crate::ui::i18n::{L10nKey, t}; use crate::ui::presets; use crate::ui::presets::Fill; #[cfg(target_os = "windows")] use std::sync::OnceLock; +/// Where macOS puts the window's traffic lights over the tab strip: nine +/// points in from the left edge, and centred on the row of chrome tiles they +/// share the bar with. +/// +/// `y` is the gap between the window's top edge and the *top* of the button +/// frame, and macOS draws those buttons 14 points tall — so the lights' centre +/// falls seven points below it. The tiles sit in the middle of the bar's +/// content box instead, which is `TITLE_BAR_HEIGHT` less the hairline +/// `TitleBar` draws along its bottom edge. Deriving the number from those two +/// facts rather than writing it out is what keeps the lights on the tile line: +/// the value the 40-point bar needed was rewritten by hand when the bar grew +/// to 48, without the border in it, and left the lights half a point — one +/// device pixel on a Retina panel — below everything beside them. pub(crate) fn traffic_light_position() -> Point { - point(px(9.), px(17.)) + // AppKit's own button frame, and the border `TitleBar` adds to the bar it + // wraps. Neither moves with the bar's height. + const BUTTON_H: f32 = 14.; + const BAR_BORDER: f32 = 1.; + + point(px(9.), px((TITLE_BAR_HEIGHT - BAR_BORDER - BUTTON_H) / 2.)) } pub(crate) fn set_menus(cx: &mut App) { @@ -1022,6 +1041,18 @@ mod tests { use super::*; use gpui::TestAppContext; + /// The lights share a line with the chrome tiles they sit beside. gpui + /// hangs the top of the 14pt frame `y` below the window's top edge, so + /// their centre is seven points under that; the tiles are centred in the + /// bar's content box, its height less the hairline `TitleBar` draws along + /// the bottom. Sixteen and a half points today, out of the 48pt bar. + #[test] + fn the_traffic_lights_sit_on_the_chrome_rows_centre() { + let position = traffic_light_position(); + assert_eq!(position.x, px(9.)); + assert_eq!(position.y + px(7.), px((TITLE_BAR_HEIGHT - 1.) / 2.)); + } + #[gpui::test] fn effective_preset_follows_the_cached_system_appearance(cx: &mut TestAppContext) { cx.update(|cx| {