diff --git a/src/ui/app.rs b/src/ui/app.rs index 2a2c7a57..0bce6917 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1170,17 +1170,26 @@ fn clear_window_override_values(config: &mut Config, backdrop_is_local: bool) { /// it and leaving takes it away. struct FullscreenHint; -/// Whether fullscreen takes the title bar away on this platform. +/// Whether the title bar carries minimize, maximize and close right now. /// -/// Not on macOS, where fullscreen belongs to the system rather than to the app. -/// The traffic lights live on that bar, and revealing the menu bar draws a -/// translucent strip over the same band, which the bar absorbs; without it the -/// strip lands on the terminal instead and covers its first row. There is also -/// nothing there to fix: `WindowControls` draws no minimize, maximize or close -/// on macOS — the three that are dead in fullscreen elsewhere are the system's -/// there, and it hides them itself. So the bar is not broken chrome on macOS, -/// it is part of how the system dresses a fullscreen window. -const FULLSCREEN_TAKES_THE_TITLE_BAR: bool = !cfg!(target_os = "macos"); +/// Not in fullscreen. A fullscreen window has no caption: Windows clears +/// `WS_CAPTION` and answers `HTCLIENT` along the whole top edge, so the three +/// buttons would draw, light up under the pointer and do nothing when clicked. +/// The row they sit at the end of stays, because it is also the tab strip. +/// +/// Never on macOS, which draws no buttons of its own: those are the system's +/// traffic lights, and the system hides them itself. +pub(crate) fn window_controls_drawn(fullscreen: bool) -> bool { + !cfg!(target_os = "macos") && !fullscreen +} + +/// How much of the title bar's trailing end the window buttons take. +pub(crate) fn window_controls_w(fullscreen: bool) -> f32 { + match window_controls_drawn(fullscreen) { + true => WINDOW_CONTROLS_W, + false => 0., + } +} impl Tty7App { pub fn for_workspace( @@ -3545,7 +3554,7 @@ impl Tty7App { /// Toggle fullscreen, and say how to leave it on the way in. /// /// Only on the way in, and only from the action: entering is an instant in - /// which the title bar disappears, and a window that starts fullscreen + /// which the window buttons disappear, and a window that starts fullscreen /// because the setting says so is not a surprise anybody needs explaining. /// The chord comes from the keymap rather than from a string, because it is /// `F11` on Windows and Linux, `Cmd+Enter` on macOS, and either of them may @@ -3563,14 +3572,14 @@ impl Tty7App { let entering = !window.is_fullscreen(); window.toggle_fullscreen(); window.remove_notification::(cx); - // Nothing disappeared where the bar stays, so there is nothing to - // explain. - if !entering || !FULLSCREEN_TAKES_THE_TITLE_BAR { + // Nothing disappeared where there were no buttons to begin with, so + // there is nothing to explain. + if !entering || !window_controls_drawn(false) { return; } let hint = match crate::ui::home::key_hint("ToggleFullscreen", cx) { Some(chord) => t_fmt(L10nKey::AppFullscreenEntered, &[("key", &chord)]), - // Rebound to nothing at all: still worth saying the bar is gone, + // Rebound to nothing at all: still worth saying the buttons are gone, // just without naming a key that would not work. None => t(L10nKey::AppFullscreenEnteredNoKey).to_string(), }; @@ -7665,22 +7674,41 @@ impl Render for Tty7App { } }; - // No title bar in fullscreen. The bar is window chrome — a caption to - // drag the window by and the three controls at its end — and a - // fullscreen window has none of that to offer: it has no caption for - // the platform to hit-test, so the buttons draw, light up under the - // pointer and do nothing at all when clicked. Drawing chrome that - // cannot work is worse than drawing none, and taking it away is also - // what the mode is for. - let fullscreen = window.is_fullscreen(); - let bar_is_gone = fullscreen && FULLSCREEN_TAKES_THE_TITLE_BAR; - let title_bar = (!bar_is_gone).then(|| { - TitleBar::new() - .h(px(TITLE_BAR_HEIGHT)) - .bg(cx.theme().transparent) - .border_color(cx.theme().transparent) - .child(strip) - }); + // No window buttons in fullscreen, where they cannot work: the window + // has no caption for the platform to hit-test, so they would draw, + // light up under the pointer and do nothing when clicked. `TitleBar` + // always draws them, so the strip goes into a plain row of the same + // geometry instead — the row itself stays, since it holds the tabs, + // the chrome tiles and the docked document's header. + let title_bar = + if window_controls_drawn(window.is_fullscreen()) || cfg!(target_os = "macos") { + TitleBar::new() + .h(px(TITLE_BAR_HEIGHT)) + .bg(cx.theme().transparent) + .border_color(cx.theme().transparent) + .child(strip) + .into_any_element() + } else { + div() + .flex_shrink_0() + .flex() + .flex_row() + .items_center() + .h(px(TITLE_BAR_HEIGHT)) + .pl(px(TITLE_BAR_LEAD)) + .border_b_1() + .border_color(cx.theme().transparent) + .child( + div() + .flex() + .flex_row() + .items_center() + .h_full() + .flex_1() + .child(strip), + ) + .into_any_element() + }; let body_area = div() .flex_1() .relative() @@ -7784,9 +7812,9 @@ impl Render for Tty7App { let panel_below_title_bar = (right_panel.is_some() || document_column.is_some()) && !cfg!(target_os = "macos"); let (column_title_bar, spanning_title_bar) = if panel_below_title_bar { - (None, title_bar) + (None, Some(title_bar)) } else { - (title_bar, None) + (Some(title_bar), None) }; let (column_overlays, hoisted_overlays) = if panel_below_title_bar { (Vec::new(), overlays) @@ -7882,7 +7910,9 @@ impl Render for Tty7App { .right(px(panel_px)) .w(px(document_px)) .when(panel_px <= 0., |d| { - d.pr(px(crate::ui::tab_strip::trailing_chrome_w())) + d.pr(px(crate::ui::tab_strip::trailing_chrome_w( + window.is_fullscreen(), + ))) }) .child(header), ) @@ -9468,6 +9498,24 @@ mod tests { assert_eq!(band.size.height, px(TITLE_BAR_HEIGHT)); } + /// Fullscreen takes the window buttons and nothing else: the strip keeps + /// its row, and the room reserved for the buttons at its end comes back. + /// macOS never had any to take. + #[test] + fn fullscreen_drops_the_window_buttons_but_not_their_row() { + assert!(!super::window_controls_drawn(true)); + assert_eq!(super::window_controls_w(true), 0.); + assert_eq!( + super::window_controls_drawn(false), + !cfg!(target_os = "macos") + ); + assert_eq!(super::window_controls_w(false), super::WINDOW_CONTROLS_W); + assert_eq!( + crate::ui::tab_strip::trailing_chrome_w(true), + crate::ui::tab_strip::trailing_chrome_tiles_w() + ); + } + /// A surface narrower than its own shadow is only reachable mid-resize, but /// a negative width would make `Bounds::contains` answer for a rectangle /// that is inside out. diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 47f63081..fd769eba 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1582,7 +1582,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::AppTabsNotRestored => "{count} tabs from last time could not be reopened", L10nKey::AppFullscreenEntered => "Fullscreen — press {key} to leave", L10nKey::AppFullscreenEnteredNoKey => { - "Fullscreen — the title bar is hidden until you leave" + "Fullscreen — the window buttons are hidden until you leave" } L10nKey::LaunchWorkspacesLeftRunning => { "Only this window was restored — {count} workspaces are still running in the background. Reopen them from the sidebar." diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 59eefef9..b254e184 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1644,7 +1644,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppOpenTerminalFailed => "ターミナルを開けませんでした: {error}", L10nKey::AppTabsNotRestored => "前回のタブ {count} 個を開き直せませんでした", L10nKey::AppFullscreenEntered => "全画面表示 — 解除するには {key}", - L10nKey::AppFullscreenEnteredNoKey => "全画面表示 — 解除するまでタイトルバーは非表示です", + L10nKey::AppFullscreenEnteredNoKey => { + "全画面表示 — 解除するまでウィンドウボタンは非表示です" + } L10nKey::LaunchWorkspacesLeftRunning => { "このウィンドウだけを復元しました — あと {count} 個のワークスペースがバックグラウンドで実行中です。サイドバーから開き直せます。" } diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index efce02fb..faa8a4e0 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1497,7 +1497,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppOpenTerminalFailed => "无法打开终端:{error}", L10nKey::AppTabsNotRestored => "上次的 {count} 个标签页没能重新打开", L10nKey::AppFullscreenEntered => "已进入全屏 —— 按 {key} 退出", - L10nKey::AppFullscreenEnteredNoKey => "已进入全屏 —— 标题栏在退出前会一直隐藏", + L10nKey::AppFullscreenEnteredNoKey => "已进入全屏 —— 窗口按钮在退出前会一直隐藏", L10nKey::LaunchWorkspacesLeftRunning => { "只恢复了这个窗口——还有 {count} 个工作区在后台运行,可从侧边栏重新打开。" } diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs index 3acdbb31..6fb2d808 100644 --- a/src/ui/keymap.rs +++ b/src/ui/keymap.rs @@ -1339,55 +1339,6 @@ mod tests { use super::*; use gpui::Action as _; - /// Everything the title bar offers a button for stays reachable from the - /// keyboard, because on Windows and Linux the title bar is not drawn in - /// fullscreen at all — it is window chrome there, and a fullscreen window - /// has no chrome for the platform to hit-test, so its buttons would light - /// up under the pointer and do nothing when clicked. (On macOS the bar - /// stays: fullscreen is the system's there, and the bar is where it puts - /// the traffic lights.) - /// - /// Reachable means either a chord of its own or a seat in the palette, - /// which has one; both are hands-free, and the palette is how the sidebar - /// toggle is reached, since it ships without a chord. What this pins is - /// that a control on that bar is never mouse-only — if one ever is, - /// hiding the bar would take a feature away with it, and this is where - /// that should be noticed. - #[test] - fn what_the_title_bar_offers_is_reachable_without_it() { - let defaults = default_bindings(); - let chord = |action: &str| { - defaults - .iter() - .any(|(name, keystroke)| *name == action && !keystroke.is_empty()) - }; - // The palette is the fallback, so it is the one that must not be. - assert!( - chord("TogglePalette"), - "the fallback needs a chord of its own" - ); - for action in [ - "NewTab", - "ToggleTabSidebar", - "OpenSettings", - "ToggleFullscreen", - "ToggleSwitcher", - ] { - assert!( - chord(action) || palette_lists(action), - "{action} would be mouse-only once the bar is hidden" - ); - } - } - - /// Whether the palette lists `action` under a name somebody wrote, which is - /// what having a real seat there means: `action_entry` answers for every - /// action, falling back to a name split on capitals, and a fallback name is - /// not evidence that anyone meant the action to be found. - fn palette_lists(action: &str) -> bool { - authored_entry(action).is_some() - } - /// The actions a keymap built from `action_bindings` dispatches for `keys` /// typed in `context`, in precedence order — the same lookup gpui performs /// on a real keypress. diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 48676609..7ef322b1 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -626,8 +626,8 @@ pub(crate) fn trailing_chrome_tiles_w() -> f32 { /// Anything else drawn into that end of the title bar has to stop short of it — /// which for the hoisted document header means the case where the detail panel /// is closed and the document column runs to the window's right edge. -pub(crate) fn trailing_chrome_w() -> f32 { - trailing_chrome_tiles_w() + crate::ui::app::WINDOW_CONTROLS_W +pub(crate) fn trailing_chrome_w(fullscreen: bool) -> f32 { + trailing_chrome_tiles_w() + crate::ui::app::window_controls_w(fullscreen) } pub(crate) fn chrome_tile_sized( @@ -1812,14 +1812,15 @@ impl Tty7App { // instead: that header carries no fill of its own, and a chip left // under it showed through the file name while staying clickable. let document_w = self.document_dock_px(window, cx).unwrap_or(0.); + let controls_w = crate::ui::app::window_controls_w(window.is_fullscreen()); let strip_w = if cfg!(target_os = "macos") { (window.viewport_size().width - px(80. + panel_w + document_w)).max(px(160.)) } else { - (window.viewport_size().width - px(114.)).max(px(140.)) + (window.viewport_size().width - px(crate::ui::app::TITLE_BAR_LEAD + controls_w)) + .max(px(140.)) }; - let chrome_band_w = (!cfg!(target_os = "macos") && self.right_panel_open(cx)).then(|| { - (self.right_panel_px(window, cx) - crate::ui::app::WINDOW_CONTROLS_W - 1.).max(0.) - }); + let chrome_band_w = (!cfg!(target_os = "macos") && self.right_panel_open(cx)) + .then(|| (self.right_panel_px(window, cx) - controls_w - 1.).max(0.)); // `corner_w` reserves the trailing window chrome. With the panel open on // macOS that chrome belongs to the panel's own header, which the strip // now stops short of, so reserving for it here would charge the chips