gpui_macos: Add simple fullscreen mode that covers the notch (#60020)

Closes #60013

# Objective

Right now Zed can go full screen but it does not allow you to fix the
hole screen,
by that I mean that Zed can go behind the notch so you don't have extra
useless room left.

## Solution

You can now use the `fullscreen_mode` = `simple` setting to use the new
simple full screen feature, that lives besides the normal full screen
feature. But allows you to have an option to go 100% full screen without
losing any useless space on your macbook screen.
**Note** this is mostly usefull when you have a macbook that has a notch
whitch is kinda in the way of your work flow.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

**Before**
<img width="5712" height="4284" alt="IMG_0339"
src="https://github.com/user-attachments/assets/9f908ffd-7cef-4999-a454-c80f72c40dc8"
/>

**After** (Note now Zed is behind your notch when using the simple full
screen feature)
<img width="5712" height="4284" alt="IMG_0360"
src="https://github.com/user-attachments/assets/5917ed4d-2a64-4464-a794-bc46fd034521"
/>


---

Release Notes:

- Added support for simple fullscreen mode using the `fullscreen_mode`
setting, set it to `simple` to try it out.
This commit is contained in:
Remco Smits authored and GitHub committed 2026-08-18 07:00:34 +00:00
1 parent f652da59e8
commit e8fcbd476d
14 files changed
+302 -32

No files matched your search

+8
View File
@@ -550,6 +550,14 @@
},
// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
"use_system_window_tabs": false,
// Which fullscreen mode the `zed::ToggleFullScreen` action enters (macOS only).
//
// 1. Use macOS's native fullscreen, which moves the window into its own Mission Control space.
// "native"
// 2. Resize the window to cover the entire screen, including the menu bar and,
// on notched displays, the area around the notch.
// "simple"
"fullscreen_mode": "native",
// Titlebar related settings
"title_bar": {
// Whether to show git status indicators on the branch icon in the titlebar.
+1 -1
View File
@@ -857,7 +857,7 @@ impl ThreadsArchiveView {
settings::SidebarSide::Left
);
let sidebar_on_right = !sidebar_on_left;
let not_fullscreen = !window.is_fullscreen();
let not_fullscreen = !window.is_fullscreen() && !window.is_simple_fullscreen();
let traffic_lights = cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
let left_window_controls = !cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
let right_window_controls =
+4
View File
@@ -865,6 +865,10 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
}
fn set_edited(&mut self, _edited: bool) {}
fn set_document_path(&self, _path: Option<&std::path::Path>) {}
fn toggle_simple_fullscreen(&self) {}
fn is_simple_fullscreen(&self) -> bool {
false
}
#[cfg(target_os = "macos")]
fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
fn show_character_palette(&self) {}
+15
View File
@@ -2457,6 +2457,13 @@ impl Window {
self.platform_window.is_fullscreen()
}
/// Returns whether the window is currently in simple (borderless) fullscreen,
/// where it covers the entire screen including the menu bar and notch area.
/// Always `false` on platforms other than macOS.
pub fn is_simple_fullscreen(&self) -> bool {
self.platform_window.is_simple_fullscreen()
}
pub(crate) fn appearance_changed(&mut self, cx: &mut App) {
self.appearance = self.platform_window.appearance();
@@ -5694,6 +5701,14 @@ impl Window {
self.platform_window.toggle_fullscreen();
}
/// Toggle simple (borderless) fullscreen, where the window covers the entire
/// screen including the menu bar and, on notched displays, the area around the
/// notch. Unlike [`Window::toggle_fullscreen`], this does not move the window
/// into its own Mission Control space. Only has an effect on macOS.
pub fn toggle_simple_fullscreen(&self) {
self.platform_window.toggle_simple_fullscreen();
}
/// Updates the IME panel position suggestions for languages like japanese, chinese.
pub fn invalidate_character_coordinates(&self) {
self.on_next_frame(|window, cx| {
+158 -2
View File
@@ -497,6 +497,93 @@ struct TrafficLightButtons {
zoom: Retained<Objc2NSButton>,
}
// `NSApplicationPresentationOptions` bits (see `NSApplication.PresentationOptions`).
const NS_APPLICATION_PRESENTATION_AUTO_HIDE_DOCK: NSUInteger = 1 << 0;
const NS_APPLICATION_PRESENTATION_AUTO_HIDE_MENU_BAR: NSUInteger = 1 << 2;
// State captured when entering simple (borderless) fullscreen, used to restore
// the window on exit.
struct SimpleFullscreenState {
frame: NSRect,
style_mask: NSWindowStyleMask,
}
enum SimpleFullscreenPlan {
Enter { screen_frame: NSRect },
Exit(SimpleFullscreenState),
}
struct SimpleFullscreenAppState {
window_count: usize,
saved_presentation_options: NSUInteger,
}
static SIMPLE_FULLSCREEN_APP_STATE: Mutex<Option<SimpleFullscreenAppState>> = Mutex::new(None);
unsafe fn push_simple_fullscreen_presentation_options() {
let mut app_state = SIMPLE_FULLSCREEN_APP_STATE.lock();
match app_state.as_mut() {
Some(app_state) => app_state.window_count += 1,
None => unsafe {
let app = NSApplication::sharedApplication(nil);
let saved_presentation_options: NSUInteger = msg_send![app, presentationOptions];
let _: () = msg_send![
app,
setPresentationOptions: NS_APPLICATION_PRESENTATION_AUTO_HIDE_DOCK
| NS_APPLICATION_PRESENTATION_AUTO_HIDE_MENU_BAR
];
*app_state = Some(SimpleFullscreenAppState {
window_count: 1,
saved_presentation_options,
});
},
}
}
unsafe fn pop_simple_fullscreen_presentation_options() {
let mut app_state = SIMPLE_FULLSCREEN_APP_STATE.lock();
if let Some(state) = app_state.as_mut() {
state.window_count = state.window_count.saturating_sub(1);
if state.window_count == 0 {
unsafe {
let app = NSApplication::sharedApplication(nil);
let _: () = msg_send![
app,
setPresentationOptions: state.saved_presentation_options
];
}
*app_state = None;
}
}
}
unsafe fn apply_simple_fullscreen_plan(
native_window: id,
native_view: id,
plan: SimpleFullscreenPlan,
) {
unsafe {
match plan {
SimpleFullscreenPlan::Exit(saved) => {
pop_simple_fullscreen_presentation_options();
native_window.setStyleMask_(saved.style_mask);
native_window.setFrame_display_(saved.frame, YES);
}
SimpleFullscreenPlan::Enter { screen_frame } => {
push_simple_fullscreen_presentation_options();
native_window.setStyleMask_(NSWindowStyleMask::NSBorderlessWindowMask);
native_window.setFrame_display_(screen_frame, YES);
}
}
// Changing the style mask makes AppKit resign the window's key status and
// first responder, so keyboard input stops reaching the editor. Re-make the
// window key and restore the GPUI view as first responder.
native_window.makeKeyAndOrderFront_(nil);
native_window.makeFirstResponder_(native_view);
}
}
struct MacWindowState {
handle: AnyWindowHandle,
foreground_executor: ForegroundExecutor,
@@ -536,6 +623,7 @@ struct MacWindowState {
// windows draw their own titlebar and move the window via `start_window_move`.
app_owns_titlebar_drag: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
simple_fullscreen_state: Option<SimpleFullscreenState>,
move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
merge_all_windows_callback: Option<Box<dyn FnMut()>>,
select_next_tab_callback: Option<Box<dyn FnMut()>>,
@@ -715,6 +803,31 @@ impl MacWindowState {
}
}
fn toggle_simple_fullscreen(&mut self) -> Option<SimpleFullscreenPlan> {
// If the window is in native fullscreen, simple fullscreen would conflict
// with AppKit's own fullscreen handling, so ignore the request.
if self.is_fullscreen() {
return None;
}
if let Some(saved) = self.simple_fullscreen_state.take() {
Some(SimpleFullscreenPlan::Exit(saved))
} else {
let screen = unsafe { self.native_window.screen() };
if screen == nil {
return None;
}
let screen_frame = unsafe { NSScreen::frame(screen) };
self.simple_fullscreen_state = Some(SimpleFullscreenState {
frame: unsafe { NSWindow::frame(self.native_window) },
style_mask: unsafe { self.native_window.styleMask() },
});
Some(SimpleFullscreenPlan::Enter { screen_frame })
}
}
fn bounds(&self) -> Bounds<Pixels> {
let mut window_frame = unsafe { NSWindow::frame(self.native_window) };
let screen = unsafe { NSWindow::screen(self.native_window) };
@@ -937,6 +1050,7 @@ impl MacWindow {
first_mouse: false,
app_owns_titlebar_drag,
fullscreen_restore_bounds: Bounds::default(),
simple_fullscreen_state: None,
move_tab_to_new_window_callback: None,
merge_all_windows_callback: None,
select_next_tab_callback: None,
@@ -1651,6 +1765,35 @@ impl PlatformWindow for MacWindow {
.detach();
}
fn toggle_simple_fullscreen(&self) {
let state = self.0.clone();
let (foreground_executor, closed) = {
let this = self.0.lock();
(this.foreground_executor.clone(), this.closed.clone())
};
foreground_executor
.spawn(async move {
if_window_not_closed(closed, move || {
let (native_window, native_view, plan) = {
let mut lock = state.lock();
(
lock.native_window,
lock.native_view.as_ptr() as id,
lock.toggle_simple_fullscreen(),
)
};
if let Some(plan) = plan {
unsafe { apply_simple_fullscreen_plan(native_window, native_view, plan) };
}
})
})
.detach();
}
fn is_simple_fullscreen(&self) -> bool {
self.0.lock().simple_fullscreen_state.is_some()
}
fn is_fullscreen(&self) -> bool {
let this = self.0.lock();
let window = this.native_window;
@@ -1786,6 +1929,9 @@ impl PlatformWindow for MacWindow {
fn titlebar_double_click(&self, is_resizable: bool, is_minimizable: bool) {
let this = self.0.lock();
if this.simple_fullscreen_state.is_some() {
return;
}
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
@@ -1843,6 +1989,9 @@ impl PlatformWindow for MacWindow {
fn start_window_move(&self) {
let this = self.0.lock();
if this.simple_fullscreen_state.is_some() {
return;
}
let window = this.native_window;
unsafe {
@@ -2751,13 +2900,20 @@ extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
extern "C" fn close_window(this: &Object, _: Sel) {
unsafe {
let close_callback = {
let (close_callback, simple_fullscreen_state) = {
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
lock.closed.store(true, Ordering::Release);
lock.close_callback.take()
(
lock.close_callback.take(),
lock.simple_fullscreen_state.take(),
)
};
if simple_fullscreen_state.is_some() {
pop_simple_fullscreen_presentation_options();
}
if let Some(callback) = close_callback {
callback();
}
@@ -245,7 +245,7 @@ impl Render for PlatformTitleBar {
.map(|this| {
let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left);
if window.is_fullscreen() {
if window.is_fullscreen() || window.is_simple_fullscreen() {
this.pl_2()
} else if self.platform_style == PlatformStyle::Mac && show_left_controls {
this.pl(px(TRAFFIC_LIGHT_PADDING))
@@ -296,33 +296,36 @@ impl Render for PlatformTitleBar {
.w_full()
.children(children),
)
.when(!window.is_fullscreen(), |title_bar| {
let show_right_controls = !(sidebar.open && sidebar.side == SidebarSide::Right);
.when(
!window.is_fullscreen() && !window.is_simple_fullscreen(),
|title_bar| {
let show_right_controls = !(sidebar.open && sidebar.side == SidebarSide::Right);
let title_bar = title_bar.children(
show_right_controls
.then(|| {
render_right_window_controls(
button_layout,
close_action.as_ref().boxed_clone(),
window,
)
})
.flatten(),
);
let title_bar = title_bar.children(
show_right_controls
.then(|| {
render_right_window_controls(
button_layout,
close_action.as_ref().boxed_clone(),
window,
)
})
.flatten(),
);
if self.platform_style == PlatformStyle::Linux
&& matches!(decorations, Decorations::Client { .. })
{
title_bar.when(supported_controls.window_menu, |titlebar| {
titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
window.show_window_menu(ev.position)
if self.platform_style == PlatformStyle::Linux
&& matches!(decorations, Decorations::Client { .. })
{
title_bar.when(supported_controls.window_menu, |titlebar| {
titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
window.show_window_menu(ev.position)
})
})
})
} else {
title_bar
}
});
} else {
title_bar
}
},
);
v_flex()
.w_full()
+7
View File
@@ -1064,6 +1064,13 @@ impl VsCodeSettings {
use_system_path_prompts: self.read_bool("files.simpleDialog.enable").map(|b| !b),
use_system_prompts: None,
use_system_window_tabs: self.read_bool("window.nativeTabs"),
fullscreen_mode: self.read_bool("window.nativeFullScreen").map(|b| {
if b {
FullscreenMode::Native
} else {
FullscreenMode::Simple
}
}),
when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
if b {
CloseWindowWhenNoItems::CloseWindow
+28
View File
@@ -121,6 +121,10 @@ pub struct WorkspaceSettingsContent {
///
/// Default: false
pub use_system_window_tabs: Option<bool>,
/// Which fullscreen mode the `zed::ToggleFullScreen` action enters (macOS only).
///
/// Default: native
pub fullscreen_mode: Option<FullscreenMode>,
/// Whether to show padding for zoomed panels.
/// When enabled, zoomed bottom panels will have some top padding,
/// while zoomed left/right panels will have padding to the right/left (respectively).
@@ -338,6 +342,30 @@ pub enum BottomDockLayout {
RightAligned,
}
#[derive(
Copy,
Clone,
Default,
Debug,
Serialize,
Deserialize,
PartialEq,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum FullscreenMode {
/// Use macOS's native fullscreen, which moves the window into its own
/// Mission Control space.
#[default]
Native,
/// Resize the window to cover the entire screen, including the menu bar and,
/// on notched displays, the area around the notch.
Simple,
}
/// Configures what draws Zed's window decorations on Linux.
/// This setting has no effect on other platforms.
#[derive(
+15 -1
View File
@@ -4886,7 +4886,7 @@ fn window_and_layout_page() -> SettingsPage {
]
}
fn window_section() -> [SettingsPageItem; 3] {
fn window_section() -> [SettingsPageItem; 4] {
[
SettingsPageItem::SectionHeader("Window"),
// todo(settings_ui): Should we filter by platform.as_ref()?
@@ -4906,6 +4906,20 @@ fn window_and_layout_page() -> SettingsPage {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Fullscreen Mode",
description: "(macOS only) which fullscreen mode the toggle fullscreen action enters.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("fullscreen_mode"),
pick: |settings_content| settings_content.workspace.fullscreen_mode.as_ref(),
write: |settings_content, value, _| {
settings_content.workspace.fullscreen_mode = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Window Decorations",
description: "(Linux only) whether Zed or your compositor should draw window decorations.",
+1
View File
@@ -641,6 +641,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::EditPredictionsMode>(render_dropdown)
.add_basic_renderer::<settings::RelativeLineNumbers>(render_dropdown)
.add_basic_renderer::<settings::WindowDecorations>(render_dropdown)
.add_basic_renderer::<settings::FullscreenMode>(render_dropdown)
.add_basic_renderer::<settings::WindowButtonLayoutContentDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::ScanSymlinksSetting>(render_dropdown)
.add_basic_renderer::<settings::FontSize>(render_editable_number_field)
+1 -1
View File
@@ -7203,7 +7203,7 @@ impl Sidebar {
let has_query = self.has_filter_query(cx);
let sidebar_on_left = self.side(cx) == SidebarSide::Left;
let sidebar_on_right = self.side(cx) == SidebarSide::Right;
let not_fullscreen = !window.is_fullscreen();
let not_fullscreen = !window.is_fullscreen() && !window.is_simple_fullscreen();
let traffic_lights = cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
let left_window_controls = !cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
let right_window_controls =
@@ -38,6 +38,7 @@ pub struct WorkspaceSettings {
pub close_on_file_delete: bool,
pub close_panel_on_toggle: bool,
pub use_system_window_tabs: bool,
pub fullscreen_mode: settings::FullscreenMode,
pub zoomed_padding: bool,
pub window_decorations: settings::WindowDecorations,
pub focus_follows_mouse: FocusFollowsMouse,
@@ -126,6 +127,7 @@ impl Settings for WorkspaceSettings {
close_on_file_delete: workspace.close_on_file_delete.unwrap(),
close_panel_on_toggle: workspace.close_panel_on_toggle.unwrap(),
use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
fullscreen_mode: workspace.fullscreen_mode.unwrap(),
zoomed_padding: workspace.zoomed_padding.unwrap(),
window_decorations: workspace.window_decorations.unwrap(),
focus_follows_mouse: FocusFollowsMouse {
+10 -2
View File
@@ -1025,8 +1025,16 @@ fn register_actions(
.register_action(|_, _: &Zoom, window, _| {
window.zoom_window();
})
.register_action(|_, _: &ToggleFullScreen, window, _| {
window.toggle_fullscreen();
.register_action(|_, _: &ToggleFullScreen, window, cx| {
let use_simple_fullscreen = window.is_simple_fullscreen()
|| (!window.is_fullscreen()
&& WorkspaceSettings::get_global(cx).fullscreen_mode
== settings::FullscreenMode::Simple);
if use_simple_fullscreen {
window.toggle_simple_fullscreen();
} else {
window.toggle_fullscreen();
}
})
.register_action(|_, _: &zed_actions::dev::ToggleFpsOverlay, window, _| {
window.cycle_debug_frame_overlay_mode();
+24
View File
@@ -1693,6 +1693,30 @@ Each option controls displaying of a particular toolbar element. If all elements
This setting enables integration with macOS’s native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS.
## Fullscreen Mode
- Description: Which fullscreen mode the `zed::ToggleFullScreen` action enters (macOS only).
- Setting: `fullscreen_mode`
- Default: `native`
**Options**
1. Use macOS's native fullscreen, which moves the window into its own Mission Control space:
```json
{
"fullscreen_mode": "native"
}
```
2. Resize the window to cover the entire screen, including the menu bar and, on notched displays, the area around the notch:
```json
{
"fullscreen_mode": "simple"
}
```
## Enable Language Server
- Description: Whether or not to use language servers to provide code intelligence.