diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index 5a656e52..1f42346d 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -21,6 +21,8 @@ pub struct CosmicCompConfig { /// If set to Global, autotile applies to all windows in all workspaces /// If set to PerWorkspace, autotile only applies to new windows, and new workspaces pub autotile_behavior: TileBehavior, + /// Configure behavior of the stack layout. + pub stack_behavior: StackBehavior, /// Active hint enabled pub active_hint: bool, /// Enables changing keyboard focus to windows when the cursor passes into them @@ -55,6 +57,7 @@ impl Default for CosmicCompConfig { xkb_config: Default::default(), autotile: Default::default(), autotile_behavior: Default::default(), + stack_behavior: StackBehavior::default(), active_hint: true, focus_follows_cursor: false, cursor_follows_focus: false, @@ -105,3 +108,16 @@ fn default_repeat_rate() -> u32 { fn default_repeat_delay() -> u32 { 600 } + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub struct StackBehavior { + pub close_tab_on_middle_click: bool, +} + +impl Default for StackBehavior { + fn default() -> Self { + Self { + close_tab_on_middle_click: true, + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index f8bd9a2f..bfab41af 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -41,7 +41,8 @@ mod types; pub use self::types::*; use cosmic::config::CosmicTk; use cosmic_comp_config::{ - input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, TileBehavior, XkbConfig, + input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, StackBehavior, TileBehavior, + XkbConfig, }; #[derive(Debug)] @@ -706,6 +707,18 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut ); } } + "stack_behavior" => { + let new = get_config::(&config, "stack_behavior"); + if new != state.common.config.cosmic_conf.stack_behavior { + state.common.config.cosmic_conf.stack_behavior = new.clone(); + + let mut shell = state.common.shell.write().unwrap(); + let shell_ref = &mut *shell; + shell_ref + .workspaces + .update_stack_behavior(new, shell_ref.seats.iter()) + } + } "active_hint" => { let new = get_config::(&config, "active_hint"); if new != state.common.config.cosmic_conf.active_hint { diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs index dfcb87e3..f6a4d899 100644 --- a/src/shell/element/mod.rs +++ b/src/shell/element/mod.rs @@ -4,6 +4,7 @@ use crate::{ utils::{iced::IcedElementInternal, prelude::*}, }; use calloop::LoopHandle; +use cosmic_comp_config::StackBehavior; use id_tree::NodeId; use smithay::{ backend::{ @@ -545,6 +546,7 @@ impl CosmicMapped { &mut self, (output, overlap): (&Output, Rectangle), theme: cosmic::Theme, + config: StackBehavior, ) { match &self.element { CosmicMappedInternal::Window(window) => { @@ -552,7 +554,7 @@ impl CosmicMapped { let activated = surface.is_activated(true); let handle = window.loop_handle(); - let stack = CosmicStack::new(std::iter::once(surface), handle, theme); + let stack = CosmicStack::new(std::iter::once(surface), handle, theme, config); if let Some(geo) = self.last_geometry.lock().unwrap().clone() { stack.set_geometry(geo.to_global(&output)); } @@ -875,6 +877,13 @@ impl CosmicMapped { _ => unreachable!(), } } + + pub fn update_stack_behavior(&mut self, behavior: &StackBehavior) { + if let CosmicMappedInternal::Stack(stack) = &mut self.element { + let mut inner = stack.0 .0.lock().unwrap(); + inner.update_stack_behavior(behavior); + } + } } impl IsAlive for CosmicMapped { diff --git a/src/shell/element/stack.rs b/src/shell/element/stack.rs index da49f5b2..0a750715 100644 --- a/src/shell/element/stack.rs +++ b/src/shell/element/stack.rs @@ -23,6 +23,7 @@ use cosmic::{ iced_widget::scrollable::AbsoluteOffset, theme, widget as cosmic_widget, Apply, Element as CosmicElement, Theme, }; +use cosmic_comp_config::StackBehavior; use cosmic_settings_config::shortcuts; use once_cell::sync::Lazy; use shortcuts::action::{Direction, FocusDirection}; @@ -105,6 +106,8 @@ pub struct CosmicStackInternal { last_seat: Arc, Serial)>>>, geometry: Arc>>>, mask: Arc>>, + + behavior: StackBehavior, } impl CosmicStackInternal { @@ -116,6 +119,10 @@ impl CosmicStackInternal { pub fn current_focus(&self) -> Option { unsafe { Focus::from_u8(self.pointer_entered.load(Ordering::SeqCst)) } } + + pub fn update_behavior(&mut self, behavior: StackBehavior) { + self.behavior = behavior; + } } pub const TAB_HEIGHT: i32 = 24; @@ -132,6 +139,7 @@ impl CosmicStack { windows: impl Iterator, handle: LoopHandle<'static, crate::state::State>, theme: cosmic::Theme, + behavior: StackBehavior, ) -> CosmicStack { let windows = windows.map(Into::into).collect::>(); assert!(!windows.is_empty()); @@ -158,6 +166,7 @@ impl CosmicStack { last_seat: Arc::new(Mutex::new(None)), geometry: Arc::new(Mutex::new(None)), mask: Arc::new(Mutex::new(None)), + behavior, }, (width, TAB_HEIGHT), handle, @@ -722,6 +731,7 @@ pub enum Message { Menu, TabMenu(usize), PotentialTabDragStart(usize), + UpdateStackBehavior(StackBehavior), Activate(usize), Close(usize), ScrollForward, @@ -969,14 +979,20 @@ impl Program for CosmicStackInternal { windows.iter().enumerate().map(|(i, w)| { let user_data = w.user_data(); user_data.insert_if_missing(Id::unique); - Tab::new( + let mut tab = Tab::new( w.title(), w.app_id(), user_data.get::().unwrap().clone(), ) .on_press(Message::PotentialTabDragStart(i)) .on_right_click(Message::TabMenu(i)) - .on_close(Message::Close(i)) + .on_close(Message::Close(i)); + + if self.behavior.close_tab_on_middle_click { + tab = tab.on_middle_click(Message::Close(i)); + } + + tab }), active, windows[active].is_activated(false), diff --git a/src/shell/element/stack/tab.rs b/src/shell/element/stack/tab.rs index f3532a72..ed40fc98 100644 --- a/src/shell/element/stack/tab.rs +++ b/src/shell/element/stack/tab.rs @@ -143,6 +143,7 @@ pub struct Tab { close_message: Option, press_message: Option, right_click_message: Option, + middle_click_message: Option, rule_theme: TabRuleTheme, background_theme: TabBackgroundTheme, active: bool, @@ -158,6 +159,7 @@ impl Tab { close_message: None, press_message: None, right_click_message: None, + middle_click_message: None, rule_theme: TabRuleTheme::Default, background_theme: TabBackgroundTheme::Default, active: false, @@ -174,6 +176,11 @@ impl Tab { self } + pub fn on_middle_click(mut self, message: Message) -> Self { + self.middle_click_message = Some(message); + self + } + pub fn on_close(mut self, message: Message) -> Self { self.close_message = Some(message); self @@ -248,6 +255,7 @@ impl Tab { elements: items, press_message: self.press_message, right_click_message: self.right_click_message, + middle_click_message: self.middle_click_message, } } } @@ -267,6 +275,7 @@ pub(super) struct TabInternal<'a, Message: TabMessage> { elements: Vec>, press_message: Option, right_click_message: Option, + middle_click_message: Option, } impl<'a, Message> Widget for TabInternal<'a, Message> @@ -412,6 +421,16 @@ where shell.publish(Message::activate(self.idx)); return event::Status::Captured; } + + if matches!( + event, + event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) + ) { + if let Some(message) = self.middle_click_message.clone() { + shell.publish(message); + return event::Status::Captured; + } + } } status diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs index 4507d7ea..ddfda2f9 100644 --- a/src/shell/layout/floating/mod.rs +++ b/src/shell/layout/floating/mod.rs @@ -6,6 +6,7 @@ use std::{ time::{Duration, Instant}, }; +use cosmic_comp_config::StackBehavior; use cosmic_settings_config::shortcuts::action::ResizeDirection; use keyframe::{ease, functions::EaseInOutCubic}; use smithay::{ @@ -1000,6 +1001,7 @@ impl FloatingLayout { &mut self, mapped: &CosmicMapped, mut focus_stack: FocusStackMut, + stack_behavior: &StackBehavior, ) -> Option { if !self.space.elements().any(|m| m == mapped) { return None; @@ -1013,7 +1015,11 @@ impl FloatingLayout { if mapped.is_window() { // if it is just a window self.space.unmap_elem(&mapped); - mapped.convert_to_stack((&output, mapped.bbox()), self.theme.clone()); + mapped.convert_to_stack( + (&output, mapped.bbox()), + self.theme.clone(), + stack_behavior.clone(), + ); self.map_internal( mapped.clone(), Some(location.as_local()), @@ -1071,13 +1077,14 @@ impl FloatingLayout { &mut self, seat: &Seat, focus_stack: FocusStackMut, + stack_behavior: &StackBehavior, ) -> Option { let Some(KeyboardFocusTarget::Element(elem)) = seat.get_keyboard().unwrap().current_focus() else { return None; }; - self.toggle_stacking(&elem, focus_stack) + self.toggle_stacking(&elem, focus_stack, stack_behavior) } pub fn move_element( diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 395a3466..86cbf5c8 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -38,6 +38,7 @@ use crate::{ }, }; +use cosmic_comp_config::StackBehavior; use cosmic_settings_config::shortcuts::action::{FocusDirection, ResizeDirection}; use id_tree::{InsertBehavior, MoveBehavior, Node, NodeId, NodeIdError, RemoveBehavior, Tree}; use keyframe::{ @@ -135,6 +136,7 @@ pub struct TilingLayout { swapping_stack_surface_id: Id, last_overview_hover: Option<(Option, TargetZone)>, pub theme: cosmic::Theme, + pub stack_behavior: StackBehavior, } #[derive(Debug, Clone, PartialEq)] @@ -342,7 +344,11 @@ pub struct MinimizedTilingState { } impl TilingLayout { - pub fn new(theme: cosmic::Theme, output: &Output) -> TilingLayout { + pub fn new( + theme: cosmic::Theme, + output: &Output, + stack_behavior: StackBehavior, + ) -> TilingLayout { TilingLayout { queue: TreeQueue { trees: { @@ -358,6 +364,7 @@ impl TilingLayout { swapping_stack_surface_id: Id::new(), last_overview_hover: None, theme, + stack_behavior, } } @@ -2121,6 +2128,7 @@ impl TilingLayout { &mut self, mapped: &CosmicMapped, mut focus_stack: FocusStackMut, + stack_behavior: &StackBehavior, ) -> Option { let gaps = self.gaps(); @@ -2137,7 +2145,11 @@ impl TilingLayout { // if it is just a window match tree.get_mut(&node_id).unwrap().data_mut() { Data::Mapped { mapped, .. } => { - mapped.convert_to_stack((&self.output, mapped.bbox()), self.theme.clone()); + mapped.convert_to_stack( + (&self.output, mapped.bbox()), + self.theme.clone(), + stack_behavior.clone(), + ); focus_stack.append(&mapped); KeyboardFocusTarget::Element(mapped.clone()) } @@ -2238,6 +2250,7 @@ impl TilingLayout { &mut self, seat: &Seat, mut focus_stack: FocusStackMut, + stack_behavior: &StackBehavior, ) -> Option { let gaps = self.gaps(); @@ -2251,7 +2264,7 @@ impl TilingLayout { { match last_active_data { FocusedNodeData::Window(mapped) => { - return self.toggle_stacking(&mapped, focus_stack); + return self.toggle_stacking(&mapped, focus_stack, stack_behavior); } FocusedNodeData::Group(_, _) => { let mut handle = None; @@ -2274,7 +2287,12 @@ impl TilingLayout { return None; } let handle = handle.unwrap(); - let stack = CosmicStack::new(surfaces.into_iter(), handle, self.theme.clone()); + let stack = CosmicStack::new( + surfaces.into_iter(), + handle, + self.theme.clone(), + stack_behavior.clone(), + ); for child in tree .children_ids(&last_active) @@ -2741,7 +2759,11 @@ impl TilingLayout { Some(TargetZone::WindowStack(window_id, _)) if tree.get(&window_id).is_ok() => { match tree.get_mut(window_id).unwrap().data_mut() { Data::Mapped { mapped, .. } => { - mapped.convert_to_stack((&self.output, mapped.bbox()), self.theme.clone()); + mapped.convert_to_stack( + (&self.output, mapped.bbox()), + self.theme.clone(), + self.stack_behavior.clone(), + ); let Some(stack) = mapped.stack_ref_mut() else { unreachable!() }; diff --git a/src/shell/mod.rs b/src/shell/mod.rs index e9022c67..446a25a4 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -13,7 +13,7 @@ use wayland_backend::server::ClientId; use crate::wayland::{handlers::data_device, protocols::workspace::WorkspaceCapabilities}; use cosmic_comp_config::{ workspace::{WorkspaceLayout, WorkspaceMode}, - TileBehavior, + StackBehavior, TileBehavior, }; use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::{ State as WState, TilingState, @@ -317,6 +317,7 @@ pub struct WorkspaceSet { pub sticky_layer: FloatingLayout, pub minimized_windows: Vec, pub workspaces: Vec, + stack_behavior: StackBehavior, } fn create_workspace( @@ -326,6 +327,7 @@ fn create_workspace( active: bool, tiling: bool, theme: cosmic::Theme, + stack_behavior: StackBehavior, ) -> Workspace { let workspace_handle = state .create_workspace( @@ -344,7 +346,13 @@ fn create_workspace( &workspace_handle, [WorkspaceCapabilities::Activate].into_iter(), ); - Workspace::new(workspace_handle, output.clone(), tiling, theme.clone()) + Workspace::new( + workspace_handle, + output.clone(), + tiling, + theme.clone(), + stack_behavior, + ) } fn move_workspace_to_group( @@ -415,6 +423,7 @@ impl WorkspaceSet { idx: usize, tiling_enabled: bool, theme: cosmic::Theme, + stack_behavior: StackBehavior, ) -> WorkspaceSet { let group_handle = state.create_workspace_group(); let sticky_layer = FloatingLayout::new(theme.clone(), output); @@ -430,6 +439,7 @@ impl WorkspaceSet { minimized_windows: Vec::new(), workspaces: Vec::new(), output: output.clone(), + stack_behavior, } } @@ -540,6 +550,7 @@ impl WorkspaceSet { false, self.tiling_enabled, self.theme.clone(), + self.stack_behavior.clone(), ); workspace_set_idx( state, @@ -603,6 +614,7 @@ pub struct Workspaces { mode: WorkspaceMode, autotile: bool, autotile_behavior: TileBehavior, + stack_behavior: StackBehavior, theme: cosmic::Theme, } @@ -615,6 +627,7 @@ impl Workspaces { mode: config.cosmic_conf.workspaces.workspace_mode, autotile: config.cosmic_conf.autotile, autotile_behavior: config.cosmic_conf.autotile_behavior, + stack_behavior: config.cosmic_conf.stack_behavior.clone(), theme, } } @@ -643,6 +656,7 @@ impl Workspaces { self.sets.len(), self.autotile, self.theme.clone(), + self.stack_behavior.clone(), ) }); workspace_state.add_group_output(&set.group, &output); @@ -855,6 +869,7 @@ impl Workspaces { false, config.cosmic_conf.autotile, self.theme.clone(), + self.stack_behavior.clone(), ), ); } @@ -1123,6 +1138,34 @@ impl Workspaces { self.autotile = autotile; self.apply_tile_change(guard, seats); } + + pub fn update_stack_behavior<'a>( + &mut self, + behavior: StackBehavior, + seats: impl Iterator>, + ) { + let seats = seats.cloned().collect::>(); + self.stack_behavior = behavior.clone(); + for (_, set) in &mut self.sets { + set.stack_behavior = behavior.clone(); + + for w in &mut set.workspaces { + w.tiling_layer.stack_behavior = behavior.clone(); + for seat in &seats { + let stack = w.focus_stack.get_mut(seat); + *stack.0 = stack + .0 + .clone() + .into_iter() + .map(|mut window| { + window.update_stack_behavior(&behavior); + window + }) + .collect(); + } + } + } + } } #[derive(Debug)] @@ -3466,6 +3509,8 @@ impl Shell { seat: &Seat, window: &CosmicMapped, ) -> Option { + let behavior = self.workspaces.stack_behavior.clone(); + if let Some(set) = self .workspaces .sets @@ -3474,16 +3519,20 @@ impl Shell { { let workspace = &mut set.workspaces[set.active]; set.sticky_layer - .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + .toggle_stacking(window, workspace.focus_stack.get_mut(seat), &behavior) } else if let Some(workspace) = self.space_for_mut(window) { if workspace.tiling_layer.mapped().any(|(m, _)| m == window) { - workspace - .tiling_layer - .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + workspace.tiling_layer.toggle_stacking( + window, + workspace.focus_stack.get_mut(seat), + &behavior, + ) } else if workspace.floating_layer.mapped().any(|w| w == window) { - workspace - .floating_layer - .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + workspace.floating_layer.toggle_stacking( + window, + workspace.focus_stack.get_mut(seat), + &behavior, + ) } else { None } @@ -3507,16 +3556,23 @@ impl Shell { } let res = if set.sticky_layer.mapped().any(|m| m == &window) { - set.sticky_layer - .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + set.sticky_layer.toggle_stacking_focused( + seat, + workspace.focus_stack.get_mut(seat), + &self.workspaces.stack_behavior, + ) } else if workspace.tiling_layer.mapped().any(|(m, _)| m == &window) { - workspace - .tiling_layer - .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + workspace.tiling_layer.toggle_stacking_focused( + seat, + workspace.focus_stack.get_mut(seat), + &self.workspaces.stack_behavior, + ) } else if workspace.floating_layer.mapped().any(|w| w == &window) { - workspace - .floating_layer - .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + workspace.floating_layer.toggle_stacking_focused( + seat, + workspace.focus_stack.get_mut(seat), + &self.workspaces.stack_behavior, + ) } else { None }; diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index 05e71538..91823c8e 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -19,6 +19,7 @@ use crate::{ }; use cosmic::theme::CosmicTheme; +use cosmic_comp_config::StackBehavior; use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::TilingState; use id_tree::Tree; use indexmap::IndexSet; @@ -238,8 +239,9 @@ impl Workspace { output: Output, tiling_enabled: bool, theme: cosmic::Theme, + stack_behavior: StackBehavior, ) -> Workspace { - let tiling_layer = TilingLayout::new(theme.clone(), &output); + let tiling_layer = TilingLayout::new(theme.clone(), &output, stack_behavior); let floating_layer = FloatingLayout::new(theme, &output); let output_name = output.name(); diff --git a/src/utils/iced.rs b/src/utils/iced.rs index 90f5bab9..9858edce 100644 --- a/src/utils/iced.rs +++ b/src/utils/iced.rs @@ -19,12 +19,14 @@ use cosmic::{ }, iced_core::{clipboard::Null as NullClipboard, id::Id, renderer::Style, Color, Length, Pixels}, iced_runtime::{ + self, program::{Program as IcedProgram, State}, task::into_stream, Action, Debug, }, Theme, }; +use cosmic_comp_config::StackBehavior; use iced_tiny_skia::{ graphics::{damage, Viewport}, Layer, @@ -68,6 +70,8 @@ use smithay::{ }, }; +use crate::shell::element::stack::CosmicStackInternal; + static ID: Lazy = Lazy::new(|| Id::new("Program")); pub struct IcedElement(pub(crate) Arc>>); @@ -408,6 +412,16 @@ impl IcedElementInternal

{ } } +impl IcedElementInternal { + pub fn update_stack_behavior(&mut self, behavior: &StackBehavior) { + self.state.queue_message( + ::Message::UpdateStackBehavior( + behavior.clone(), + ), + ); + } +} + impl PointerTarget for IcedElement

{ fn enter( &self,