Closes https://github.com/zed-industries/zed/issues/14722 Closes https://github.com/zed-industries/zed/issues/4948 Closes https://github.com/zed-industries/zed/issues/7136 Follow up of https://github.com/zed-industries/zed/pull/20557 and https://github.com/zed-industries/zed/pull/32238. Based on the discussions in the previous PRs and the pairing session with @ConradIrwin I've decided to rewrite it from scratch, to properly incorporate all the requirements. The feature is opt-in, the settings is set to false by default. Once enabled via the Zed settings, it will behave according to the user’s system preference, without requiring a restart — the next window opened will adopt the new behavior (similar to Ghostty). I’m not entirely sure if the changes to the Window class are the best approach. I’ve tried to keep things flexible enough that other applications built with GPUI won’t be affected (while giving them the option to still use it), but I’d appreciate input on whether this direction makes sense long-term. https://github.com/user-attachments/assets/9573e094-4394-41ad-930c-5375a8204cbf ### Features * System-aware tabbing behavior * Respects the three system modes: Always, Never, and Fullscreen (default on macOS) * Changing the Zed setting does not require a restart — the next window reflects the change * Full theme support * Integrates with light and dark themes * [One Dark](https://github.com/user-attachments/assets/d1f55ff7-2339-4b09-9faf-d3d610ba7ca2) * [One Light](https://github.com/user-attachments/assets/7776e30c-2686-493e-9598-cdcd7e476ecf) * Supports opaque/blurred/transparent themes as best as possible * [One Dark - blurred](https://github.com/user-attachments/assets/c4521311-66cb-4cee-9e37-15146f6869aa) * Dynamic layout adjustments * Only reserves tab bar space when tabs are actually visible * [With tabs](https://github.com/user-attachments/assets/3b6db943-58c5-4f55-bdf4-33d23ca7d820) * [Without tabs](https://github.com/user-attachments/assets/2d175959-5efc-4e4f-a15c-0108925c582e) * VS Code compatibility * Supports the `window.nativeTabs` setting in the VS Code settings importer * Command palette integration * Adds commands for managing tabs to the command palette * These can be assigned to keyboard shortcuts as well, but didn't add defaults as to not reserve precious default key combinations Happy to pair again if things can be improved codewise, or if explanations are necessary for certain choices! Release Notes: * Added support for native macOS window tabbing. When you set `"use_system_window_tabs": true`, Zed will merge windows in the same was as macOS: by default this happens only when full screened, but you can adjust your macOS settings to have this happen on all windows. --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
220 lines
7.2 KiB
Rust
220 lines
7.2 KiB
Rust
use gpui::{
|
|
App, Application, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window,
|
|
WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*,
|
|
px, rgb,
|
|
};
|
|
|
|
struct WindowContent {
|
|
text: SharedString,
|
|
bounds: Bounds<Pixels>,
|
|
bg: Hsla,
|
|
}
|
|
|
|
impl Render for WindowContent {
|
|
fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let window_bounds = window.bounds();
|
|
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.bg(self.bg)
|
|
.size_full()
|
|
.items_center()
|
|
.text_color(rgb(0xffffff))
|
|
.child(self.text.clone())
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.text_sm()
|
|
.items_center()
|
|
.size_full()
|
|
.child(format!(
|
|
"origin: {}, {} size: {}, {}",
|
|
self.bounds.origin.x,
|
|
self.bounds.origin.y,
|
|
self.bounds.size.width,
|
|
self.bounds.size.height
|
|
))
|
|
.child(format!(
|
|
"cx.bounds() origin: {}, {} size {}, {}",
|
|
window_bounds.origin.x,
|
|
window_bounds.origin.y,
|
|
window_bounds.size.width,
|
|
window_bounds.size.height
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
|
|
WindowOptions {
|
|
// Set the bounds of the window in screen coordinates
|
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
|
// Specify the display_id to ensure the window is created on the correct screen
|
|
display_id: Some(display_id),
|
|
titlebar: None,
|
|
window_background: WindowBackgroundAppearance::Transparent,
|
|
focus: false,
|
|
show: true,
|
|
kind: WindowKind::PopUp,
|
|
is_movable: false,
|
|
app_id: None,
|
|
window_min_size: None,
|
|
window_decorations: None,
|
|
tabbing_identifier: None,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
Application::new().run(|cx: &mut App| {
|
|
// Create several new windows, positioned in the top right corner of each screen
|
|
let size = Size {
|
|
width: px(350.),
|
|
height: px(75.),
|
|
};
|
|
let margin_offset = px(150.);
|
|
|
|
for screen in cx.displays() {
|
|
let bounds = Bounds {
|
|
origin: point(margin_offset, margin_offset),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Top Left {:?}", screen.id()).into(),
|
|
bg: gpui::red(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: screen.bounds().top_right()
|
|
- point(size.width + margin_offset, -margin_offset),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Top Right {:?}", screen.id()).into(),
|
|
bg: gpui::red(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: screen.bounds().bottom_left()
|
|
- point(-margin_offset, size.height + margin_offset),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Bottom Left {:?}", screen.id()).into(),
|
|
bg: gpui::blue(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: screen.bounds().bottom_right()
|
|
- point(size.width + margin_offset, size.height + margin_offset),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Bottom Right {:?}", screen.id()).into(),
|
|
bg: gpui::blue(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Top Center {:?}", screen.id()).into(),
|
|
bg: gpui::black(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Left Center {:?}", screen.id()).into(),
|
|
bg: gpui::black(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: point(
|
|
screen.bounds().center().x - size.center().x,
|
|
screen.bounds().center().y - size.center().y,
|
|
),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Center {:?}", screen.id()).into(),
|
|
bg: gpui::black(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: point(
|
|
screen.bounds().size.width - size.width - margin_offset,
|
|
screen.bounds().center().y - size.center().y,
|
|
),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Right Center {:?}", screen.id()).into(),
|
|
bg: gpui::black(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
let bounds = Bounds {
|
|
origin: point(
|
|
screen.bounds().center().x - size.center().x,
|
|
screen.bounds().size.height - size.height - margin_offset,
|
|
),
|
|
size,
|
|
};
|
|
|
|
cx.open_window(build_window_options(screen.id(), bounds), |_, cx| {
|
|
cx.new(|_| WindowContent {
|
|
text: format!("Bottom Center {:?}", screen.id()).into(),
|
|
bg: gpui::black(),
|
|
bounds,
|
|
})
|
|
})
|
|
.unwrap();
|
|
}
|
|
});
|
|
}
|