diff --git a/src/ui/app.rs b/src/ui/app.rs index 96f97ead..54d5a7eb 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1167,6 +1167,31 @@ fn clear_window_override_values(config: &mut Config, backdrop_is_local: bool) { } } +/// The id the fullscreen hint is pushed under, so that entering again replaces +/// it and leaving takes it away. +struct FullscreenHint; + +/// Whether the title bar carries minimize, maximize and close right now. +/// +/// 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( id: Option, @@ -3530,6 +3555,46 @@ impl Tty7App { remember_leaf_in(&mut self.tabs, leaf); } + /// 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 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 + /// have been rebound. + /// + /// The hint carries an id of its own, which is what keeps a held-down + /// `F11` to one notice rather than a column of identical ones: pushing + /// under an id already on screen replaces that one. Leaving through the + /// action takes it back too, so a quick in-and-out does not leave the way + /// out on screen after it has been taken. Leaving some other way — a + /// window manager with a chord of its own — just lets it time out, which + /// is a second or two of a stale notice and not worth watching every + /// frame for. + fn toggle_fullscreen(&self, window: &mut Window, cx: &mut App) { + let entering = !window.is_fullscreen(); + window.toggle_fullscreen(); + window.remove_notification::(cx); + // 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 buttons are gone, + // just without naming a key that would not work. + None => t(L10nKey::AppFullscreenEnteredNoKey).to_string(), + }; + window.push_notification( + gpui_component::notification::Notification::new() + .id::() + .message(hint), + cx, + ); + } + fn focus_leaf(&self, leaf: &PaneSlot, window: &mut Window, cx: &mut App) { let handle = leaf.focus_handle(cx); window.focus(&handle, cx); @@ -5362,7 +5427,7 @@ impl Tty7App { NextTab => self.cycle_tab(true, window, cx), PrevTab => self.cycle_tab(false, window, cx), ToggleMaximizePane => self.toggle_maximize(window, cx), - ToggleFullscreen => window.toggle_fullscreen(), + ToggleFullscreen => self.toggle_fullscreen(window, cx), ToggleTabSidebar => self.toggle_tab_sidebar(cx), ToggleLeftPanel => self.toggle_left_panel(cx), ToggleRightPanel => self.toggle_right_panel(cx), @@ -7629,11 +7694,41 @@ impl Render for Tty7App { } }; - let title_bar = 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() @@ -7835,7 +7930,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), ) @@ -8032,9 +8129,9 @@ impl Render for Tty7App { .on_action(cx.listener(|this, _: &ToggleMaximizePane, window, cx| { this.toggle_maximize(window, cx) })) - .on_action( - cx.listener(|_, _: &ToggleFullscreen, window, _cx| window.toggle_fullscreen()), - ) + .on_action(cx.listener(|this, _: &ToggleFullscreen, window, cx| { + this.toggle_fullscreen(window, cx) + })) .on_action(cx.listener(|this, _: &ToggleTabSidebar, _window, cx| { this.toggle_tab_sidebar(cx) })) @@ -9421,6 +9518,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 2ee8ac3e..a2f97082 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1597,6 +1597,10 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::AppReopenTabFailed => "Could not reopen the tab: no terminal started", L10nKey::AppOpenTerminalFailed => "Could not open a terminal: {error}", L10nKey::AppTabsNotRestored => "{count} tabs from last time could not be reopened", + L10nKey::AppFullscreenEntered => "Fullscreen — press {key} to leave", + L10nKey::AppFullscreenEnteredNoKey => { + "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 63ada063..ee452f64 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1662,6 +1662,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::AppReopenTabFailed => "タブを開き直せませんでした: ターミナルが起動しませんでした", L10nKey::AppOpenTerminalFailed => "ターミナルを開けませんでした: {error}", L10nKey::AppTabsNotRestored => "前回のタブ {count} 個を開き直せませんでした", + L10nKey::AppFullscreenEntered => "全画面表示 — 解除するには {key}", + L10nKey::AppFullscreenEnteredNoKey => { + "全画面表示 — 解除するまでウィンドウボタンは非表示です" + } L10nKey::LaunchWorkspacesLeftRunning => { "このウィンドウだけを復元しました — あと {count} 個のワークスペースがバックグラウンドで実行中です。サイドバーから開き直せます。" } diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 72f05b87..df0010b3 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -1290,6 +1290,8 @@ l10n_keys! { AppReopenTabFailed, AppOpenTerminalFailed, AppTabsNotRestored, + AppFullscreenEntered, + AppFullscreenEnteredNoKey, LaunchWorkspacesLeftRunning, AppSshConnectionFailed, AppSshReconnectFailed, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 47932e3f..17790008 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1507,6 +1507,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::AppReopenTabFailed => "无法重新打开标签页:没有启动终端", L10nKey::AppOpenTerminalFailed => "无法打开终端:{error}", L10nKey::AppTabsNotRestored => "上次的 {count} 个标签页没能重新打开", + L10nKey::AppFullscreenEntered => "已进入全屏 —— 按 {key} 退出", + L10nKey::AppFullscreenEnteredNoKey => "已进入全屏 —— 窗口按钮在退出前会一直隐藏", L10nKey::LaunchWorkspacesLeftRunning => { "只恢复了这个窗口——还有 {count} 个工作区在后台运行,可从侧边栏重新打开。" } 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