logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
[package]
name = "platform_title_bar"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/platform_title_bar.rs"
doctest = false
[dependencies]
gpui.workspace = true
project.workspace = true
settings.workspace = true
smallvec.workspace = true
theme.workspace = true
theme_settings.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,333 @@
pub mod platforms;
mod system_window_tabs;
use gpui::{
Action, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement, StatefulInteractiveElement, Styled, WeakEntity, Window,
WindowButtonLayout, WindowControlArea, div, px,
};
use project::DisableAiSettings;
use settings::Settings;
use smallvec::SmallVec;
use std::mem;
use ui::{
prelude::*,
utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
};
use workspace::{MultiWorkspace, SidebarRenderState, SidebarSide};
use crate::{
platforms::{platform_linux, platform_windows},
system_window_tabs::SystemWindowTabs,
};
pub use system_window_tabs::{
DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, ShowNextWindowTab, ShowPreviousWindowTab,
};
pub struct PlatformTitleBar {
id: ElementId,
platform_style: PlatformStyle,
children: SmallVec<[AnyElement; 2]>,
should_move: bool,
system_window_tabs: Entity<SystemWindowTabs>,
button_layout: Option<WindowButtonLayout>,
multi_workspace: Option<WeakEntity<MultiWorkspace>>,
}
impl PlatformTitleBar {
pub fn new(id: impl Into<ElementId>, cx: &mut Context<Self>) -> Self {
let platform_style = PlatformStyle::platform();
let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new());
Self {
id: id.into(),
platform_style,
children: SmallVec::new(),
should_move: false,
system_window_tabs,
button_layout: None,
multi_workspace: None,
}
}
pub fn with_multi_workspace(mut self, multi_workspace: WeakEntity<MultiWorkspace>) -> Self {
self.multi_workspace = Some(multi_workspace);
self
}
pub fn set_multi_workspace(&mut self, multi_workspace: WeakEntity<MultiWorkspace>) {
self.multi_workspace = Some(multi_workspace);
}
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move {
cx.theme().colors().title_bar_background
} else {
cx.theme().colors().title_bar_inactive_background
}
} else {
cx.theme().colors().title_bar_background
}
}
pub fn set_children<T>(&mut self, children: T)
where
T: IntoIterator<Item = AnyElement>,
{
self.children = children.into_iter().collect();
}
pub fn set_button_layout(&mut self, button_layout: Option<WindowButtonLayout>) {
self.button_layout = button_layout;
}
fn effective_button_layout(
&self,
decorations: &Decorations,
cx: &App,
) -> Option<WindowButtonLayout> {
if self.platform_style == PlatformStyle::Linux
&& matches!(decorations, Decorations::Client { .. })
{
self.button_layout.or_else(|| cx.button_layout())
} else {
None
}
}
pub fn init(cx: &mut App) {
SystemWindowTabs::init(cx);
}
fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
self.multi_workspace
.as_ref()
.and_then(|mw| mw.upgrade())
.map(|mw| mw.read(cx).sidebar_render_state(cx))
.unwrap_or_default()
}
pub fn is_multi_workspace_enabled(cx: &App) -> bool {
!DisableAiSettings::get_global(cx).disable_ai
}
}
/// Renders the platform-appropriate left-side window controls (e.g. Ubuntu/GNOME close button).
///
/// Only relevant on Linux with client-side decorations when the window manager
/// places controls on the left.
pub fn render_left_window_controls(
button_layout: Option<WindowButtonLayout>,
close_action: Box<dyn Action>,
window: &Window,
) -> Option<AnyElement> {
if PlatformStyle::platform() != PlatformStyle::Linux {
return None;
}
if !matches!(window.window_decorations(), Decorations::Client { .. }) {
return None;
}
let button_layout = button_layout?;
if button_layout.left[0].is_none() {
return None;
}
Some(
platform_linux::LinuxWindowControls::new(
"left-window-controls",
button_layout.left,
close_action,
)
.into_any_element(),
)
}
/// Renders the platform-appropriate right-side window controls (close, minimize, maximize).
///
/// Returns `None` on Mac or when the platform doesn't need custom controls
/// (e.g. Linux with server-side decorations).
pub fn render_right_window_controls(
button_layout: Option<WindowButtonLayout>,
close_action: Box<dyn Action>,
window: &Window,
) -> Option<AnyElement> {
let decorations = window.window_decorations();
let height = platform_title_bar_height(window);
match PlatformStyle::platform() {
PlatformStyle::Linux => {
if !matches!(decorations, Decorations::Client { .. }) {
return None;
}
let button_layout = button_layout?;
if button_layout.right[0].is_none() {
return None;
}
Some(
platform_linux::LinuxWindowControls::new(
"right-window-controls",
button_layout.right,
close_action,
)
.into_any_element(),
)
}
PlatformStyle::Windows => {
Some(platform_windows::WindowsWindowControls::new(height).into_any_element())
}
PlatformStyle::Mac => None,
}
}
impl Render for PlatformTitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let supported_controls = window.window_controls();
let decorations = window.window_decorations();
let height = platform_title_bar_height(window);
let titlebar_color = self.title_bar_color(window, cx);
let close_action = Box::new(workspace::CloseWindow);
let children = mem::take(&mut self.children);
let button_layout = self.effective_button_layout(&decorations, cx);
let sidebar = self.sidebar_render_state(cx);
let title_bar = h_flex()
.window_control_area(WindowControlArea::Drag)
.w_full()
.h(height)
.map(|this| {
this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
this.should_move = false;
}))
.on_mouse_up(
gpui::MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.should_move = false;
}),
)
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.should_move = true;
}),
)
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
if this.should_move {
this.should_move = false;
window.start_window_move();
}
}))
})
.map(|this| {
// Note: On Windows the title bar behavior is handled by the platform implementation.
this.id(self.id.clone())
.when(self.platform_style == PlatformStyle::Mac, |this| {
this.on_click(|event, window, _| {
if event.click_count() == 2 {
window.titlebar_double_click();
}
})
})
.when(self.platform_style == PlatformStyle::Linux, |this| {
this.on_click(|event, window, _| {
if event.click_count() == 2 {
window.zoom_window();
}
})
})
})
.map(|this| {
let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left);
if window.is_fullscreen() {
this.pl_2()
} else if self.platform_style == PlatformStyle::Mac && show_left_controls {
this.pl(px(TRAFFIC_LIGHT_PADDING))
} else if let Some(controls) = show_left_controls
.then(|| {
render_left_window_controls(
button_layout,
close_action.as_ref().boxed_clone(),
window,
)
})
.flatten()
{
this.child(controls)
} else {
this.pl_2()
}
})
.map(|el| match decorations {
Decorations::Server => el,
Decorations::Client { tiling, .. } => el
.when(
!(tiling.top || tiling.right)
&& !(sidebar.open && sidebar.side == SidebarSide::Right),
|el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
)
.when(
!(tiling.top || tiling.left)
&& !(sidebar.open && sidebar.side == SidebarSide::Left),
|el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
)
// this border is to avoid a transparent gap in the rounded corners
.mt(px(-1.))
.mb(px(-1.))
.border(px(1.))
.border_color(titlebar_color),
})
.bg(titlebar_color)
.content_stretch()
.child(
div()
.id(self.id.clone())
.flex()
.flex_row()
.items_center()
.justify_between()
.overflow_x_hidden()
.w_full()
.children(children),
)
.when(!window.is_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(),
);
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
}
});
v_flex()
.w_full()
.child(title_bar)
.child(self.system_window_tabs.clone().into_any_element())
}
}
impl ParentElement for PlatformTitleBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -0,0 +1,2 @@
pub mod platform_linux;
pub mod platform_windows;

View File

@@ -0,0 +1,245 @@
use gpui::{
Action, AnyElement, Hsla, MAX_BUTTONS_PER_SIDE, MouseButton, WindowButton, prelude::*, svg,
};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct LinuxWindowControls {
id: &'static str,
buttons: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
close_action: Box<dyn Action>,
}
impl LinuxWindowControls {
pub fn new(
id: &'static str,
buttons: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
close_action: Box<dyn Action>,
) -> Self {
Self {
id,
buttons,
close_action,
}
}
}
impl RenderOnce for LinuxWindowControls {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_maximized = window.is_maximized();
let supported_controls = window.window_controls();
let button_elements: Vec<AnyElement> = self
.buttons
.iter()
.filter_map(|b| *b)
.filter(|button| match button {
WindowButton::Minimize => supported_controls.minimize,
WindowButton::Maximize => supported_controls.maximize,
WindowButton::Close => true,
})
.map(|button| {
create_window_button(button, button.id(), is_maximized, &*self.close_action, cx)
})
.collect();
h_flex()
.id(self.id)
.when(!button_elements.is_empty(), |el| {
el.gap_3()
.px_3()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(button_elements)
})
}
}
fn create_window_button(
button: WindowButton,
id: &'static str,
is_maximized: bool,
close_action: &dyn Action,
cx: &mut App,
) -> AnyElement {
match button {
WindowButton::Minimize => {
WindowControl::new(id, WindowControlType::Minimize, cx).into_any_element()
}
WindowButton::Maximize => WindowControl::new(
id,
if is_maximized {
WindowControlType::Restore
} else {
WindowControlType::Maximize
},
cx,
)
.into_any_element(),
WindowButton::Close => {
WindowControl::new_close(id, WindowControlType::Close, close_action.boxed_clone(), cx)
.into_any_element()
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum WindowControlType {
Minimize,
Restore,
Maximize,
Close,
}
impl WindowControlType {
/// Returns the icon name for the window control type.
///
/// Will take a [PlatformStyle] in the future to return a different
/// icon name based on the platform.
pub fn icon(&self) -> IconName {
match self {
WindowControlType::Minimize => IconName::GenericMinimize,
WindowControlType::Restore => IconName::GenericRestore,
WindowControlType::Maximize => IconName::GenericMaximize,
WindowControlType::Close => IconName::GenericClose,
}
}
}
#[allow(unused)]
pub struct WindowControlStyle {
background: Hsla,
background_hover: Hsla,
icon: Hsla,
icon_hover: Hsla,
}
impl WindowControlStyle {
pub fn default(cx: &mut App) -> Self {
let colors = cx.theme().colors();
Self {
background: colors.ghost_element_background,
background_hover: colors.ghost_element_hover,
icon: colors.icon,
icon_hover: colors.icon_muted,
}
}
#[allow(unused)]
/// Sets the background color of the control.
pub fn background(mut self, color: impl Into<Hsla>) -> Self {
self.background = color.into();
self
}
#[allow(unused)]
/// Sets the background color of the control when hovered.
pub fn background_hover(mut self, color: impl Into<Hsla>) -> Self {
self.background_hover = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon.
pub fn icon(mut self, color: impl Into<Hsla>) -> Self {
self.icon = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon when hovered.
pub fn icon_hover(mut self, color: impl Into<Hsla>) -> Self {
self.icon_hover = color.into();
self
}
}
#[derive(IntoElement)]
pub struct WindowControl {
id: ElementId,
icon: WindowControlType,
style: WindowControlStyle,
close_action: Option<Box<dyn Action>>,
}
impl WindowControl {
pub fn new(id: impl Into<ElementId>, icon: WindowControlType, cx: &mut App) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
pub fn new_close(
id: impl Into<ElementId>,
icon: WindowControlType,
close_action: Box<dyn Action>,
cx: &mut App,
) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: Some(close_action.boxed_clone()),
}
}
#[allow(unused)]
pub fn custom_style(
id: impl Into<ElementId>,
icon: WindowControlType,
style: WindowControlStyle,
) -> Self {
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
}
impl RenderOnce for WindowControl {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let icon = svg()
.size_4()
.flex_none()
.path(self.icon.icon().path())
.text_color(self.style.icon)
.group_hover("", |this| this.text_color(self.style.icon_hover));
h_flex()
.id(self.id)
.group("")
.cursor_pointer()
.justify_center()
.content_center()
.rounded_2xl()
.w_5()
.h_5()
.hover(|this| this.bg(self.style.background_hover))
.active(|this| this.bg(self.style.background_hover))
.child(icon)
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
match self.icon {
WindowControlType::Minimize => window.minimize_window(),
WindowControlType::Restore => window.zoom_window(),
WindowControlType::Maximize => window.zoom_window(),
WindowControlType::Close => window.dispatch_action(
self.close_action
.as_ref()
.expect("Use WindowControl::new_close() for close control.")
.boxed_clone(),
cx,
),
}
})
}
}

View File

@@ -0,0 +1,137 @@
use gpui::{Hsla, Rgba, WindowControlArea, prelude::*};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct WindowsWindowControls {
button_height: Pixels,
}
impl WindowsWindowControls {
pub fn new(button_height: Pixels) -> Self {
Self { button_height }
}
#[cfg(not(target_os = "windows"))]
fn get_font() -> &'static str {
"Segoe Fluent Icons"
}
#[cfg(target_os = "windows")]
fn get_font() -> &'static str {
use windows::Wdk::System::SystemServices::RtlGetVersion;
let mut version = unsafe { std::mem::zeroed() };
let status = unsafe { RtlGetVersion(&mut version) };
if status.is_ok() && version.dwBuildNumber >= 22000 {
"Segoe Fluent Icons"
} else {
"Segoe MDL2 Assets"
}
}
}
impl RenderOnce for WindowsWindowControls {
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
div()
.id("windows-window-controls")
.font_family(Self::get_font())
.flex()
.flex_row()
.justify_center()
.content_stretch()
.max_h(self.button_height)
.min_h(self.button_height)
.child(WindowsCaptionButton::Minimize)
.map(|this| {
this.child(if window.is_maximized() {
WindowsCaptionButton::Restore
} else {
WindowsCaptionButton::Maximize
})
})
.child(WindowsCaptionButton::Close)
}
}
#[derive(IntoElement)]
enum WindowsCaptionButton {
Minimize,
Restore,
Maximize,
Close,
}
impl WindowsCaptionButton {
#[inline]
fn id(&self) -> &'static str {
match self {
Self::Minimize => "minimize",
Self::Restore => "restore",
Self::Maximize => "maximize",
Self::Close => "close",
}
}
#[inline]
fn icon(&self) -> &'static str {
match self {
Self::Minimize => "\u{e921}",
Self::Restore => "\u{e923}",
Self::Maximize => "\u{e922}",
Self::Close => "\u{e8bb}",
}
}
#[inline]
fn control_area(&self) -> WindowControlArea {
match self {
Self::Close => WindowControlArea::Close,
Self::Maximize | Self::Restore => WindowControlArea::Max,
Self::Minimize => WindowControlArea::Min,
}
}
}
impl RenderOnce for WindowsCaptionButton {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let (hover_bg, hover_fg, active_bg, active_fg) = match self {
Self::Close => {
let color: Hsla = Rgba {
r: 232.0 / 255.0,
g: 17.0 / 255.0,
b: 32.0 / 255.0,
a: 1.0,
}
.into();
(
color,
gpui::white(),
color.opacity(0.8),
gpui::white().opacity(0.8),
)
}
_ => (
cx.theme().colors().ghost_element_hover,
cx.theme().colors().text,
cx.theme().colors().ghost_element_active,
cx.theme().colors().text,
),
};
h_flex()
.id(self.id())
.justify_center()
.content_center()
.occlude()
.w(px(36.))
.h_full()
.text_size(px(10.0))
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
.active(|style| style.bg(active_bg).text_color(active_fg))
.window_control_area(self.control_area())
.child(self.icon())
}
}

View File

@@ -0,0 +1,529 @@
use settings::{Settings, SettingsStore};
use gpui::{
AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle,
Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div,
};
use theme_settings::ThemeSettings;
use ui::{
Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label,
LabelSize, Tab, h_flex, prelude::*, right_click_menu,
};
use workspace::{
CloseWindow, ItemSettings, Workspace, WorkspaceSettings,
item::{ClosePosition, ShowCloseButton},
};
actions!(
window,
[
ShowNextWindowTab,
ShowPreviousWindowTab,
MergeAllWindows,
MoveTabToNewWindow
]
);
#[derive(Clone)]
pub struct DraggedWindowTab {
pub id: WindowId,
pub ix: usize,
pub handle: AnyWindowHandle,
pub title: String,
pub width: Pixels,
pub is_active: bool,
pub active_background_color: Hsla,
pub inactive_background_color: Hsla,
}
pub struct SystemWindowTabs {
tab_bar_scroll_handle: ScrollHandle,
measured_tab_width: Pixels,
last_dragged_tab: Option<DraggedWindowTab>,
}
impl SystemWindowTabs {
pub fn new() -> Self {
Self {
tab_bar_scroll_handle: ScrollHandle::new(),
measured_tab_width: px(0.),
last_dragged_tab: None,
}
}
pub fn init(cx: &mut App) {
let mut was_use_system_window_tabs =
WorkspaceSettings::get_global(cx).use_system_window_tabs;
cx.observe_global::<SettingsStore>(move |cx| {
let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
if use_system_window_tabs == was_use_system_window_tabs {
return;
}
was_use_system_window_tabs = use_system_window_tabs;
let tabbing_identifier = if use_system_window_tabs {
Some(String::from("zed"))
} else {
None
};
if use_system_window_tabs {
SystemWindowTabController::init(cx);
}
cx.windows().iter().for_each(|handle| {
let _ = handle.update(cx, |_, window, cx| {
window.set_tabbing_identifier(tabbing_identifier.clone());
if use_system_window_tabs {
let tabs = if let Some(tabs) = window.tabbed_windows() {
tabs
} else {
vec![SystemWindowTab::new(
SharedString::from(window.window_title()),
window.window_handle(),
)]
};
SystemWindowTabController::add_tab(cx, handle.window_id(), tabs);
}
});
});
})
.detach();
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action_renderer(|div, _, window, cx| {
let window_id = window.window_handle().window_id();
let controller = cx.global::<SystemWindowTabController>();
let tab_groups = controller.tab_groups();
let tabs = controller.tabs(window_id);
let Some(tabs) = tabs else {
return div;
};
div.when(tabs.len() > 1, |div| {
div.on_action(move |_: &ShowNextWindowTab, window, cx| {
SystemWindowTabController::select_next_tab(
cx,
window.window_handle().window_id(),
);
})
.on_action(move |_: &ShowPreviousWindowTab, window, cx| {
SystemWindowTabController::select_previous_tab(
cx,
window.window_handle().window_id(),
);
})
.on_action(move |_: &MoveTabToNewWindow, window, cx| {
SystemWindowTabController::move_tab_to_new_window(
cx,
window.window_handle().window_id(),
);
window.move_tab_to_new_window();
})
})
.when(tab_groups.len() > 1, |div| {
div.on_action(move |_: &MergeAllWindows, window, cx| {
SystemWindowTabController::merge_all_windows(
cx,
window.window_handle().window_id(),
);
window.merge_all_windows();
})
})
});
})
.detach();
}
fn render_tab(
&self,
ix: usize,
item: SystemWindowTab,
tabs: Vec<SystemWindowTab>,
active_background_color: Hsla,
inactive_background_color: Hsla,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let entity = cx.entity();
let settings = ItemSettings::get_global(cx);
let close_side = &settings.close_position;
let show_close_button = &settings.show_close_button;
let rem_size = window.rem_size();
let width = self.measured_tab_width.max(rem_size * 10);
let is_active = window.window_handle().window_id() == item.id;
let title = item.title.to_string();
let label = Label::new(&title)
.size(LabelSize::Small)
.truncate()
.color(if is_active {
Color::Default
} else {
Color::Muted
});
let tab = h_flex()
.id(ix)
.group("tab")
.w_full()
.overflow_hidden()
.h(Tab::content_height(cx))
.relative()
.px(DynamicSpacing::Base16.px(cx))
.justify_center()
.border_l_1()
.border_color(cx.theme().colors().border)
.cursor_pointer()
.on_drag(
DraggedWindowTab {
id: item.id,
ix,
handle: item.handle,
title: item.title.to_string(),
width,
is_active,
active_background_color,
inactive_background_color,
},
move |tab, _, _, cx| {
entity.update(cx, |this, _cx| {
this.last_dragged_tab = Some(tab.clone());
});
cx.new(|_| tab.clone())
},
)
.drag_over::<DraggedWindowTab>({
let tab_ix = ix;
move |element, dragged_tab: &DraggedWindowTab, _, cx| {
let mut styled_tab = element
.bg(cx.theme().colors().drop_target_background)
.border_color(cx.theme().colors().drop_target_border)
.border_0();
if tab_ix < dragged_tab.ix {
styled_tab = styled_tab.border_l_2();
} else if tab_ix > dragged_tab.ix {
styled_tab = styled_tab.border_r_2();
}
styled_tab
}
})
.on_drop({
let tab_ix = ix;
cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| {
this.last_dragged_tab = None;
Self::handle_tab_drop(dragged_tab, tab_ix, cx);
})
})
.on_click(move |_, _, cx| {
let _ = item.handle.update(cx, |_, window, _| {
window.activate_window();
});
})
.on_mouse_up(MouseButton::Middle, move |_, window, cx| {
if item.handle.window_id() == window.window_handle().window_id() {
window.dispatch_action(Box::new(CloseWindow), cx);
} else {
let _ = item.handle.update(cx, |_, window, cx| {
window.dispatch_action(Box::new(CloseWindow), cx);
});
}
})
.child(label)
.map(|this| match show_close_button {
ShowCloseButton::Hidden => this,
_ => this.child(
div()
.absolute()
.top_2()
.w_4()
.h_4()
.map(|this| match close_side {
ClosePosition::Left => this.left_1(),
ClosePosition::Right => this.right_1(),
})
.child(
IconButton::new("close", IconName::Close)
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click({
move |_, window, cx| {
if item.handle.window_id()
== window.window_handle().window_id()
{
window.dispatch_action(Box::new(CloseWindow), cx);
} else {
let _ = item.handle.update(cx, |_, window, cx| {
window.dispatch_action(Box::new(CloseWindow), cx);
});
}
}
})
.map(|this| match show_close_button {
ShowCloseButton::Hover => this.visible_on_hover("tab"),
_ => this,
}),
),
),
})
.into_any();
let menu = right_click_menu(ix)
.trigger(|_, _, _| tab)
.menu(move |window, cx| {
let focus_handle = cx.focus_handle();
let tabs = tabs.clone();
let other_tabs = tabs.clone();
let move_tabs = tabs.clone();
let merge_tabs = tabs.clone();
ContextMenu::build(window, cx, move |mut menu, _window_, _cx| {
menu = menu.entry("Close Tab", None, move |window, cx| {
Self::handle_right_click_action(
cx,
window,
&tabs,
|tab| tab.id == item.id,
|window, cx| {
window.dispatch_action(Box::new(CloseWindow), cx);
},
);
});
menu = menu.entry("Close Other Tabs", None, move |window, cx| {
Self::handle_right_click_action(
cx,
window,
&other_tabs,
|tab| tab.id != item.id,
|window, cx| {
window.dispatch_action(Box::new(CloseWindow), cx);
},
);
});
menu = menu.entry("Move Tab to New Window", None, move |window, cx| {
Self::handle_right_click_action(
cx,
window,
&move_tabs,
|tab| tab.id == item.id,
|window, cx| {
SystemWindowTabController::move_tab_to_new_window(
cx,
window.window_handle().window_id(),
);
window.move_tab_to_new_window();
},
);
});
menu = menu.entry("Show All Tabs", None, move |window, cx| {
Self::handle_right_click_action(
cx,
window,
&merge_tabs,
|tab| tab.id == item.id,
|window, _cx| {
window.toggle_window_tab_overview();
},
);
});
menu.context(focus_handle)
})
});
div()
.flex_1()
.min_w(rem_size * 10)
.when(is_active, |this| this.bg(active_background_color))
.border_t_1()
.border_color(if is_active {
active_background_color
} else {
cx.theme().colors().border
})
.child(menu)
}
fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context<Self>) {
SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix);
}
fn handle_right_click_action<F, P>(
cx: &mut App,
window: &mut Window,
tabs: &Vec<SystemWindowTab>,
predicate: P,
mut action: F,
) where
P: Fn(&SystemWindowTab) -> bool,
F: FnMut(&mut Window, &mut App),
{
for tab in tabs {
if predicate(tab) {
if tab.id == window.window_handle().window_id() {
action(window, cx);
} else {
let _ = tab.handle.update(cx, |_view, window, cx| {
action(window, cx);
});
}
}
}
}
}
impl Render for SystemWindowTabs {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
let active_background_color = cx.theme().colors().title_bar_background;
let inactive_background_color = cx.theme().colors().tab_bar_background;
let entity = cx.entity();
let controller = cx.global::<SystemWindowTabController>();
let visible = controller.is_visible();
let current_window_tab = vec![SystemWindowTab::new(
SharedString::from(window.window_title()),
window.window_handle(),
)];
let tabs = controller
.tabs(window.window_handle().window_id())
.unwrap_or(&current_window_tab)
.clone();
let tab_items = tabs
.iter()
.enumerate()
.map(|(ix, item)| {
self.render_tab(
ix,
item.clone(),
tabs.clone(),
active_background_color,
inactive_background_color,
window,
cx,
)
})
.collect::<Vec<_>>();
let number_of_tabs = tab_items.len().max(1);
if (!window.tab_bar_visible() && !visible)
|| (!use_system_window_tabs && number_of_tabs == 1)
{
return h_flex().into_any_element();
}
h_flex()
.w_full()
.h(Tab::container_height(cx))
.bg(inactive_background_color)
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|this, _event, window, cx| {
if let Some(tab) = this.last_dragged_tab.take() {
SystemWindowTabController::move_tab_to_new_window(cx, tab.id);
if tab.id == window.window_handle().window_id() {
window.move_tab_to_new_window();
} else {
let _ = tab.handle.update(cx, |_, window, _cx| {
window.move_tab_to_new_window();
});
}
}
}),
)
.child(
h_flex()
.id("window tabs")
.w_full()
.h(Tab::container_height(cx))
.bg(inactive_background_color)
.overflow_x_scroll()
.track_scroll(&self.tab_bar_scroll_handle)
.children(tab_items)
.child(
canvas(
|_, _, _| (),
move |bounds, _, _, cx| {
let entity = entity.clone();
entity.update(cx, |this, cx| {
let width = bounds.size.width / number_of_tabs as f32;
if width != this.measured_tab_width {
this.measured_tab_width = width;
cx.notify();
}
});
},
)
.absolute()
.size_full(),
),
)
.child(
h_flex()
.h_full()
.px(DynamicSpacing::Base06.rems(cx))
.border_t_1()
.border_l_1()
.border_color(cx.theme().colors().border)
.child(
IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(|_event, window, cx| {
window.dispatch_action(
Box::new(zed_actions::OpenRecent {
create_new_window: true,
}),
cx,
);
}),
),
)
.into_any_element()
}
}
impl Render for DraggedWindowTab {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
let label = Label::new(self.title.clone())
.size(LabelSize::Small)
.truncate()
.color(if self.is_active {
Color::Default
} else {
Color::Muted
});
h_flex()
.h(Tab::container_height(cx))
.w(self.width)
.px(DynamicSpacing::Base16.px(cx))
.justify_center()
.bg(if self.is_active {
self.active_background_color
} else {
self.inactive_background_color
})
.border_1()
.border_color(cx.theme().colors().border)
.font(ui_font)
.child(label)
}
}