diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index ea99c40e..f30b628f 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -778,6 +778,20 @@ impl Client { above } + pub fn set_show_float_pin_icon(&self, show: bool) { + self.send(&ClientMessage::SetShowFloatPinIcon { show }); + } + + pub fn get_pinned(&self, seat: Seat) -> bool { + let res = self.send_with_response(&ClientMessage::GetFloatPinned { seat }); + get_response!(res, false, GetFloatPinned { pinned }); + pinned + } + + pub fn set_pinned(&self, seat: Seat, pinned: bool) { + self.send(&ClientMessage::SetFloatPinned { seat, pinned }); + } + pub fn connector_connected(&self, connector: Connector) -> bool { let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector }); get_response!(res, false, ConnectorConnected { connected }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 2f7ddbb8..531faae7 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -546,6 +546,16 @@ pub enum ClientMessage<'a> { above: bool, }, GetFloatAboveFullscreen, + GetFloatPinned { + seat: Seat, + }, + SetFloatPinned { + seat: Seat, + pinned: bool, + }, + SetShowFloatPinIcon { + show: bool, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -697,6 +707,9 @@ pub enum Response { GetFloatAboveFullscreen { above: bool, }, + GetFloatPinned { + pinned: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 73d31131..dfd3b433 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -452,6 +452,24 @@ impl Seat { }); }); } + + /// Gets whether the currently focused window is pinned. + /// + /// If a floating window is pinned, it will stay visible even when switching to a + /// different workspace. + pub fn float_pinned(self) -> bool { + get!().get_pinned(self) + } + + /// Sets whether the currently focused window is pinned. + pub fn set_float_pinned(self, pinned: bool) { + get!().set_pinned(self, pinned); + } + + /// Toggles whether the currently focused window is pinned. + pub fn toggle_float_pinned(self) { + self.set_float_pinned(!self.float_pinned()); + } } /// A focus-follows-mouse mode. diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 688c4df3..a2c7792c 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -43,6 +43,8 @@ )] #![warn(unsafe_op_in_unsafe_fn)] +#[expect(unused_imports)] +use crate::input::Seat; use { crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector}, serde::{Deserialize, Serialize}, @@ -292,3 +294,13 @@ pub fn get_float_above_fullscreen() -> bool { pub fn toggle_float_above_fullscreen() { set_float_above_fullscreen(!get_float_above_fullscreen()) } + +/// Sets whether floating windows always show a pin icon. +/// +/// Clicking on the pin icon toggles the pin mode. See [`Seat::toggle_float_pinned`]. +/// +/// The icon is always shown if the window is pinned. This setting only affects unpinned +/// windows. +pub fn set_show_float_pin_icon(show: bool) { + get!().set_show_float_pin_icon(show); +} diff --git a/release-notes.md b/release-notes.md index a5b2254a..4bcaf3ea 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,8 @@ by using the `enable-float-above-fullscreen` action. - Implement xdg-toplevel-tag-v1. - Implement tablet-v2 version 2. +- Floating windows can now be pinned. A pinned floating window stays visible on + its output even when switching workspaces. # 1.10.0 (2025-04-22) diff --git a/src/compositor.rs b/src/compositor.rs index f9d43f92..e94cdc05 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -289,6 +289,7 @@ fn start_compositor2( color_manager, float_above_fullscreen: Cell::new(false), icons: Default::default(), + show_pin_icon: Cell::new(false), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -618,6 +619,7 @@ fn create_dummy_output(state: &Rc) { tray_start_rel: Default::default(), tray_items: Default::default(), ext_workspace_groups: Default::default(), + pinned: Default::default(), }); let dummy_workspace = Rc::new(WorkspaceNode { id: state.node_ids.next(), diff --git a/src/config/handler.rs b/src/config/handler.rs index d4accbe9..8f39efdc 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1151,6 +1151,29 @@ impl ConfigProxyHandler { }); } + fn handle_set_show_float_pin_icon(&self, show: bool) { + self.state.show_pin_icon.set(show); + for stacked in self.state.root.stacked.iter() { + if let Some(float) = stacked.deref().clone().node_into_float() { + float.schedule_render_titles(); + } + } + } + + fn handle_get_float_pinned(&self, seat: Seat) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + self.respond(Response::GetFloatPinned { + pinned: seat.pinned(), + }); + Ok(()) + } + + fn handle_set_float_pinned(&self, seat: Seat, pinned: bool) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_pinned(pinned); + Ok(()) + } + fn handle_set_vrr_mode( &self, connector: Option, @@ -2060,6 +2083,15 @@ impl ConfigProxyHandler { self.handle_set_float_above_fullscreen(above) } ClientMessage::GetFloatAboveFullscreen => self.handle_get_float_above_fullscreen(), + ClientMessage::GetFloatPinned { seat } => { + self.handle_get_float_pinned(seat).wrn("get_float_pinned")? + } + ClientMessage::SetFloatPinned { seat, pinned } => self + .handle_set_float_pinned(seat, pinned) + .wrn("set_float_pinned")?, + ClientMessage::SetShowFloatPinIcon { show } => { + self.handle_set_show_float_pin_icon(show) + } } Ok(()) } diff --git a/src/icons.rs b/src/icons.rs index 3bcda66c..ba916498 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -28,7 +28,6 @@ pub enum IconState { Passive, } -#[expect(dead_code)] pub struct SizedIcons { pub pin_unfocused_title: StaticMap>, pub pin_focused_title: StaticMap>, diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index bb3f4415..6366f110 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -1119,6 +1119,20 @@ impl WlSeatGlobal { }; kb.phy_state.destroy(self.state.now_usec(), self); } + + pub fn pinned(&self) -> bool { + let Some(tl) = self.keyboard_node.get().node_toplevel() else { + return false; + }; + tl.tl_pinned() + } + + pub fn set_pinned(&self, pinned: bool) { + let Some(tl) = self.keyboard_node.get().node_toplevel() else { + return; + }; + tl.tl_set_pinned(true, pinned); + } } impl CursorUserOwner for WlSeatGlobal { diff --git a/src/renderer.rs b/src/renderer.rs index 5289ffe4..d49d0032 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,7 +1,7 @@ use { crate::{ gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect}, - icons::SizedIcons, + icons::{IconState, SizedIcons}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -28,7 +28,6 @@ pub struct Renderer<'a> { pub state: &'a State, pub logical_extents: Rect, pub pixel_extents: Rect, - #[expect(dead_code)] pub icons: Option>, } @@ -524,11 +523,45 @@ impl Renderer<'_> { let title_underline = [Rect::new_sized(x + bw, y + bw + th, pos.width() - 2 * bw, 1).unwrap()]; self.base.fill_boxes(&title_underline, &uc, srgb); + let rect = floating.title_rect.get().move_(x, y); + let bounds = self.base.scale_rect(rect); + let (mut x1, y1) = rect.position(); + let is_pinned = floating.pinned_link.borrow().is_some(); + if is_pinned || self.state.show_pin_icon.get() { + let (x, y) = self.base.scale_point(x1, y1); + if let Some(icons) = &self.icons { + let icon = if floating.active.get() { + &icons.pin_focused_title + } else if floating.attention_requested.get() { + &icons.pin_attention_requested + } else { + &icons.pin_unfocused_title + }; + let state = match is_pinned { + true => IconState::Active, + false => IconState::Passive, + }; + self.base.render_texture( + &icon[state], + None, + x, + y, + None, + None, + self.base.scale, + Some(&bounds), + None, + AcquireSync::None, + ReleaseSync::None, + false, + srgb_srgb, + ); + } + x1 += th; + } if let Some(title) = floating.title_textures.borrow().get(&self.base.scale) { if let Some(texture) = title.texture() { - let rect = floating.title_rect.get().move_(x, y); - let bounds = self.base.scale_rect(rect); - let (x, y) = self.base.scale_point(rect.x1(), rect.y1()); + let (x, y) = self.base.scale_point(x1, y1); self.base.render_texture( &texture, None, diff --git a/src/state.rs b/src/state.rs index 05bde3db..bc972c53 100644 --- a/src/state.rs +++ b/src/state.rs @@ -81,7 +81,7 @@ use { tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode, - ToplevelNodeBase, VrrMode, WorkspaceNode, + ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, }, utils::{ activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings, @@ -115,7 +115,7 @@ use { cell::{Cell, RefCell}, fmt::{Debug, Formatter}, mem, - ops::DerefMut, + ops::{Deref, DerefMut}, rc::{Rc, Weak}, sync::Arc, time::Duration, @@ -239,6 +239,7 @@ pub struct State { pub color_manager: Rc, pub float_above_fullscreen: Cell, pub icons: Icons, + pub show_pin_icon: Cell, } // impl Drop for State { @@ -742,8 +743,21 @@ impl State { let (output, ws) = match self.workspaces.get(name) { Some(ws) => { let output = ws.output.get(); + let mut pinned_is_focused = false; + for pinned in output.pinned.iter() { + pinned + .deref() + .clone() + .node_visit(&mut generic_node_visitor(|node| { + node.node_seat_state().for_each_kb_focus(|s| { + pinned_is_focused |= s.id() == seat.id(); + }); + })); + } let did_change = output.show_workspace(&ws); - ws.clone().node_do_focus(seat, Direction::Unspecified); + if !pinned_is_focused { + ws.clone().node_do_focus(seat, Direction::Unspecified); + } if !did_change { return; } diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 32857e54..cd714ef4 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -195,6 +195,7 @@ impl ConnectorHandler { tray_start_rel: Default::default(), tray_items: Default::default(), ext_workspace_groups: Default::default(), + pinned: Default::default(), }); on.update_visible(); on.update_rects(); diff --git a/src/tree/container.rs b/src/tree/container.rs index 91728ce6..4faee9ce 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -2041,6 +2041,14 @@ impl ContainingNode for ContainerNode { } } } + + fn cnode_pinned(&self) -> bool { + self.tl_pinned() + } + + fn cnode_set_pinned(self: Rc, pinned: bool) { + self.tl_set_pinned(false, pinned); + } } impl ToplevelNodeBase for ContainerNode { diff --git a/src/tree/containing.rs b/src/tree/containing.rs index 72b47525..009ac694 100644 --- a/src/tree/containing.rs +++ b/src/tree/containing.rs @@ -31,4 +31,10 @@ pub trait ContainingNode: Node { let _ = new_y1; let _ = new_y2; } + fn cnode_pinned(&self) -> bool { + false + } + fn cnode_set_pinned(self: Rc, pinned: bool) { + let _ = pinned; + } } diff --git a/src/tree/float.rs b/src/tree/float.rs index aff53f5d..34a67145 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -5,7 +5,7 @@ use { cursor_user::CursorUser, fixed::Fixed, ifs::wl_seat::{ - BTN_LEFT, NodeSeatState, SeatId, WlSeatGlobal, + BTN_LEFT, BTN_RIGHT, NodeSeatState, SeatId, WlSeatGlobal, tablet::{TabletTool, TabletToolChanges, TabletToolId}, }, rect::Rect, @@ -15,7 +15,7 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - OutputNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, + OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, walker::NodeVisitor, }, utils::{ @@ -25,6 +25,7 @@ use { }, }, ahash::AHashMap, + arrayvec::ArrayVec, std::{ cell::{Cell, RefCell}, fmt::{Debug, Formatter}, @@ -42,6 +43,7 @@ pub struct FloatNode { pub position: Cell, pub display_link: RefCell>>>, pub workspace_link: Cell>>>, + pub pinned_link: RefCell>>>, pub workspace: CloneCell>, pub child: CloneCell>>, pub active: Cell, @@ -120,6 +122,7 @@ impl FloatNode { position: Cell::new(position), display_link: RefCell::new(None), workspace_link: Cell::new(None), + pinned_link: RefCell::new(None), workspace: CloneCell::new(ws.clone()), child: CloneCell::new(Some(child.clone())), active: Cell::new(false), @@ -144,6 +147,9 @@ impl FloatNode { if floater.visible.get() { state.damage(position); } + if child.tl_data().pinned.get() { + floater.toggle_pinned(); + } floater } @@ -217,6 +223,9 @@ impl FloatNode { let mut th = tr.height(); let mut scalef = None; let mut width = tr.width(); + if self.state.show_pin_icon.get() || self.pinned_link.borrow().is_some() { + width = (width - th).max(0); + } if *scale != 1 { let scale = scale.to_f64(); th = (th as f64 * scale).round() as _; @@ -402,17 +411,32 @@ impl FloatNode { } } - fn set_workspace(self: &Rc, ws: &Rc) { + fn set_workspace_( + self: &Rc, + ws: &Rc, + update_pinned: bool, + update_visible: bool, + ) { if let Some(c) = self.child.get() { c.tl_set_workspace(ws); } self.workspace_link .set(Some(ws.stacked.add_last(self.clone()))); self.workspace.set(ws.clone()); - self.stacked_set_visible(ws.float_visible()); + if update_visible { + self.stacked_set_visible(ws.float_visible()); + } + if update_pinned { + if let Some(pl) = &*self.pinned_link.borrow_mut() { + ws.output.get().pinned.add_last_existing(pl); + } + } } - pub fn adjust_position_after_ws_move(self: &Rc, output: &Rc) { + pub fn after_ws_move(self: &Rc, output: &Rc) { + if let Some(pinned) = &*self.pinned_link.borrow() { + output.pinned.add_last_existing(pinned); + } if output.is_dummy { return; } @@ -505,6 +529,20 @@ impl FloatNode { } } + fn toggle_pinned(self: &Rc) { + let pl = &mut *self.pinned_link.borrow_mut(); + *pl = if pl.is_some() { + None + } else { + let output = self.workspace.get().output.get(); + Some(output.pinned.add_last(self.clone())) + }; + if let Some(tl) = self.child.get() { + tl.tl_data().pinned.set(pl.is_some()); + } + self.schedule_render_titles(); + } + fn button( self: Rc, id: CursorType, @@ -518,6 +556,34 @@ impl FloatNode { Some(s) => s, _ => return, }; + let bw = self.state.theme.sizes.border_width.get(); + let th = self.state.theme.sizes.title_height.get(); + let mut is_icon_press = false; + if pressed && cursor_data.x >= bw && cursor_data.y >= bw && cursor_data.y < bw + th { + enum FloatIcon { + Pin, + } + let mut icons = ArrayVec::::new(); + if self.state.show_pin_icon.get() || self.pinned_link.borrow().is_some() { + icons.push(FloatIcon::Pin); + } + let mut x2 = bw + th; + let icon = 'icon: { + for icon in icons { + if cursor_data.x < x2 { + break 'icon Some(icon); + } + x2 += th; + } + None + }; + if let Some(icon) = icon { + is_icon_press = true; + match icon { + FloatIcon::Pin => self.toggle_pinned(), + } + } + } if !cursor_data.op_active { if !pressed { return; @@ -533,6 +599,7 @@ impl FloatNode { cursor_data.x, cursor_data.y, ) && cursor_data.op_type == OpType::Move + && !is_icon_press { if let Some(tl) = self.child.get() { drop(cursors); @@ -572,7 +639,7 @@ impl FloatNode { } else if !pressed { cursor_data.op_active = false; let ws = cursor.output().ensure_workspace(); - self.set_workspace(&ws); + self.set_workspace_(&ws, true, true); } } @@ -681,6 +748,9 @@ impl Node for FloatNode { state: KeyState, _serial: u64, ) { + if button == BTN_RIGHT && state == KeyState::Pressed { + self.toggle_pinned(); + } if button != BTN_LEFT { return; } @@ -800,6 +870,7 @@ impl ContainingNode for FloatNode { self.child.set(None); self.display_link.borrow_mut().take(); self.workspace_link.set(None); + self.pinned_link.take(); if self.visible.get() { self.state.damage(self.position.get()); } @@ -874,6 +945,17 @@ impl ContainingNode for FloatNode { self.schedule_layout(); } } + + fn cnode_pinned(&self) -> bool { + self.pinned_link.borrow().is_some() + } + + fn cnode_set_pinned(self: Rc, pinned: bool) { + if self.pinned_link.borrow().is_some() == pinned { + return; + } + self.toggle_pinned(); + } } impl StackedNode for FloatNode { @@ -891,3 +973,9 @@ impl StackedNode for FloatNode { true } } + +impl PinnedNode for FloatNode { + fn set_workspace(self: Rc, workspace: &Rc, update_visible: bool) { + self.set_workspace_(workspace, false, update_visible); + } +} diff --git a/src/tree/output.rs b/src/tree/output.rs index 0f3e2ae8..b2af5cae 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -39,9 +39,9 @@ use { state::State, text::TextTexture, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, StackedNode, - TddType, TileDragDestination, WorkspaceDragDestination, WorkspaceNode, WorkspaceNodeId, - walker::NodeVisitor, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, PinnedNode, + StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, WorkspaceNode, + WorkspaceNodeId, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -103,6 +103,7 @@ pub struct OutputNode { pub tray_start_rel: Cell, pub tray_items: LinkedList>, pub ext_workspace_groups: CopyHashMap>, + pub pinned: LinkedList>, } #[derive(Copy, Clone, Debug, PartialEq)] @@ -646,6 +647,9 @@ impl OutputNode { return false; } collect_kb_foci2(old.clone(), &mut seats); + for pinned in self.pinned.iter() { + pinned.deref().clone().set_workspace(ws, false); + } if old.is_empty() { for jw in old.jay_workspaces.lock().values() { jw.send_destroyed(); diff --git a/src/tree/stacked.rs b/src/tree/stacked.rs index c5ad3f41..4b745005 100644 --- a/src/tree/stacked.rs +++ b/src/tree/stacked.rs @@ -1,4 +1,7 @@ -use crate::tree::Node; +use { + crate::tree::{Node, WorkspaceNode}, + std::rc::Rc, +}; pub trait StackedNode: Node { fn stacked_prepare_set_visible(&self) { @@ -14,3 +17,7 @@ pub trait StackedNode: Node { true } } + +pub trait PinnedNode: StackedNode { + fn set_workspace(self: Rc, workspace: &Rc, update_visible: bool); +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 556cc885..dd962817 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -50,6 +50,8 @@ pub trait ToplevelNode: ToplevelNodeBase { fn tl_change_extents(self: Rc, rect: &Rect); fn tl_set_visible(&self, visible: bool); fn tl_destroy(&self); + fn tl_pinned(&self) -> bool; + fn tl_set_pinned(&self, self_pinned: bool, pinned: bool); } impl ToplevelNode for T { @@ -151,6 +153,24 @@ impl ToplevelNode for T { self.tl_data().destroy_node(self); self.tl_destroy_impl(); } + + fn tl_pinned(&self) -> bool { + let Some(parent) = self.tl_data().parent.get() else { + return false; + }; + parent.cnode_pinned() + } + + fn tl_set_pinned(&self, self_pinned: bool, pinned: bool) { + let data = self.tl_data(); + if self_pinned { + data.pinned.set(pinned); + } + let Some(parent) = data.parent.get() else { + return; + }; + parent.cnode_set_pinned(pinned); + } } pub trait ToplevelNodeBase: Node { @@ -243,6 +263,7 @@ pub struct ToplevelData { pub is_floating: Cell, pub float_width: Cell, pub float_height: Cell, + pub pinned: Cell, pub is_fullscreen: Cell, pub fullscrceen_data: RefCell>, pub workspace: CloneCell>>, @@ -283,6 +304,7 @@ impl ToplevelData { is_floating: Default::default(), float_width: Default::default(), float_height: Default::default(), + pinned: Cell::new(false), is_fullscreen: Default::default(), fullscrceen_data: Default::default(), workspace: Default::default(), diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index a6822dd7..2f70be3a 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -128,7 +128,7 @@ impl WorkspaceNode { } fn visit_float(&mut self, node: &Rc) { - node.adjust_position_after_ws_move(self.0); + node.after_ws_move(self.0); node.node_visit_children(self); } @@ -426,6 +426,27 @@ pub fn move_ws_to_output( config: WsMoveConfig, ) { let source = ws.output.get(); + if let Some(visible) = source.workspace.get() { + if visible.id == ws.id { + source.workspace.take(); + } + } + let mut new_source_ws = None; + if !config.source_is_destroyed && !source.is_dummy && source.workspace.is_none() { + new_source_ws = source + .workspaces + .iter() + .find(|c| c.id != ws.id) + .map(|c| (*c).clone()); + if new_source_ws.is_none() && source.pinned.is_not_empty() { + new_source_ws = Some(source.generate_workspace()); + } + } + if let Some(new_source_ws) = &new_source_ws { + for pinned in source.pinned.iter() { + pinned.deref().clone().set_workspace(new_source_ws, false); + } + } ws.set_output(&target); 'link: { if let Some(before) = config.before { @@ -445,18 +466,9 @@ pub fn move_ws_to_output( ws.set_visible(false); } ws.flush_jay_workspaces(); - if let Some(visible) = source.workspace.get() { - if visible.id == ws.id { - source.workspace.take(); - } - } - if !config.source_is_destroyed && !source.is_dummy { - if source.workspace.is_none() { - if let Some(ws) = source.workspaces.first() { - source.show_workspace(&ws); - ws.flush_jay_workspaces(); - } - } + if let Some(ws) = new_source_ws { + source.show_workspace(&ws); + ws.flush_jay_workspaces(); } if !target.is_dummy { target.schedule_update_render_data(); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 24b492b6..9facb61a 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -14,6 +14,7 @@ use { parsers::{ color_management::ColorManagement, config::{ConfigParser, ConfigParserError}, + float::Float, }, }, toml::{self}, @@ -58,6 +59,8 @@ pub enum SimpleCommand { EnableWindowManagement(bool), SetFloatAboveFullscreen(bool), ToggleFloatAboveFullscreen, + SetFloatPinned(bool), + ToggleFloatPinned, } #[derive(Debug, Clone)] @@ -367,6 +370,7 @@ pub struct Config { pub ui_drag: UiDrag, pub xwayland: Option, pub color_management: Option, + pub float: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index a04a1ce1..b6450d25 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -16,6 +16,7 @@ mod drm_device; mod drm_device_match; mod env; pub mod exec; +pub mod float; mod format; mod gfx_api; mod idle; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 34a2160e..7131dd50 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -116,6 +116,9 @@ impl ActionParser<'_> { "enable-float-above-fullscreen" => SetFloatAboveFullscreen(true), "disable-float-above-fullscreen" => SetFloatAboveFullscreen(false), "toggle-float-above-fullscreen" => ToggleFloatAboveFullscreen, + "pin-float" => SetFloatPinned(true), + "unpin-float" => SetFloatPinned(false), + "toggle-float-pinned" => ToggleFloatPinned, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index e1ad68bc..fa627f21 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -12,6 +12,7 @@ use { drm_device::DrmDevicesParser, drm_device_match::DrmDeviceMatchParser, env::EnvParser, + float::FloatParser, gfx_api::GfxApiParser, idle::IdleParser, input::InputsParser, @@ -118,7 +119,7 @@ impl Parser for ConfigParser<'_> { ui_drag_val, xwayland_val, ), - (color_management_val,), + (color_management_val, float_val), ) = ext.extract(( ( opt(val("keymap")), @@ -156,7 +157,7 @@ impl Parser for ConfigParser<'_> { opt(val("ui-drag")), opt(val("xwayland")), ), - (opt(val("color-management")),), + (opt(val("color-management")), opt(val("float"))), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -381,6 +382,15 @@ impl Parser for ConfigParser<'_> { } } } + let mut float = None; + if let Some(value) = float_val { + match value.parse(&mut FloatParser(self.0)) { + Ok(v) => float = Some(v), + Err(e) => { + log::warn!("Could not parse the float settings: {}", self.0.error(e)); + } + } + } Ok(Config { keymap, repeat_rate, @@ -412,6 +422,7 @@ impl Parser for ConfigParser<'_> { ui_drag, xwayland, color_management, + float, }) } } diff --git a/toml-config/src/config/parsers/float.rs b/toml-config/src/config/parsers/float.rs new file mode 100644 index 00000000..fa3ccbed --- /dev/null +++ b/toml-config/src/config/parsers/float.rs @@ -0,0 +1,48 @@ +use { + crate::{ + config::{ + context::Context, + extractor::{Extractor, ExtractorError, bol, opt, recover}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum FloatParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct FloatParser<'a>(pub &'a Context<'a>); + +#[derive(Debug, Clone)] +pub struct Float { + pub show_pin_icon: Option, +} + +impl Parser for FloatParser<'_> { + type Value = Float; + type Error = FloatParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (show_pin_icon,) = ext.extract((recover(opt(bol("show-pin-icon"))),))?; + Ok(Float { + show_pin_icon: show_pin_icon.despan(), + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index cbf12998..83f21d5f 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -25,7 +25,8 @@ use { logging::set_log_level, on_devices_enumerated, on_idle, quit, reload, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, - set_idle, set_idle_grace_period, set_ui_drag_enabled, set_ui_drag_threshold, + set_idle, set_idle_grace_period, set_show_float_pin_icon, set_ui_drag_enabled, + set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, theme::{reset_colors, reset_font, reset_sizes, set_font}, @@ -101,6 +102,8 @@ impl Action { B::new(move || set_float_above_fullscreen(bool)) } SimpleCommand::ToggleFloatAboveFullscreen => B::new(toggle_float_above_fullscreen), + SimpleCommand::SetFloatPinned(pinned) => B::new(move || s.set_float_pinned(pinned)), + SimpleCommand::ToggleFloatPinned => B::new(move || s.toggle_float_pinned()), }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -1096,6 +1099,11 @@ fn load_config(initial_load: bool, persistent: &Rc) { set_color_management_enabled(enabled); } } + if let Some(float) = config.float { + if let Some(show) = float.show_pin_icon { + set_show_float_pin_icon(show); + } + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index a712c28d..3a1b0a5c 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -636,6 +636,10 @@ "color-management": { "description": "Configures the color-management settings.\n\n- Example:\n\n ```toml\n [color-management]\n enabled = true\n ```\n", "$ref": "#/$defs/ColorManagement" + }, + "float": { + "description": "Configures the settings of floating windows.\n\n- Example:\n\n ```toml\n [float]\n show-pin-icon = true\n ```\n", + "$ref": "#/$defs/Float" } }, "required": [] @@ -808,6 +812,17 @@ } ] }, + "Float": { + "description": "Describes settings of floating windows.\n\n- Example:\n\n ```toml\n [float]\n show-pin-icon = true\n ```\n", + "type": "object", + "properties": { + "show-pin-icon": { + "type": "boolean", + "description": "Sets whether floating windows always show a pin icon.\n\nThe default is `false`.\n" + } + }, + "required": [] + }, "Format": { "type": "string", "description": "A graphics format.\n\nThese formats are documented in https://github.com/torvalds/linux/blob/master/include/uapi/drm/drm_fourcc.h\n\n- Example:\n\n ```toml\n [[outputs]]\n match.serial-number = \"33K03894SL0\"\n format = \"rgb565\"\n ```\n", @@ -1284,7 +1299,10 @@ "disable-window-management", "enable-float-above-fullscreen", "disable-float-above-fullscreen", - "toggle-float-above-fullscreen" + "toggle-float-above-fullscreen", + "pin-float", + "unpin-float", + "toggle-float-pinned" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 3d19e38c..b31ee8da 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1265,6 +1265,19 @@ The table has the following fields: The value of this field should be a [ColorManagement](#types-ColorManagement). +- `float` (optional): + + Configures the settings of floating windows. + + - Example: + + ```toml + [float] + show-pin-icon = true + ``` + + The value of this field should be a [Float](#types-Float). + ### `Connector` @@ -1629,6 +1642,31 @@ The table has the following fields: The value of this field should be a boolean. + +### `Float` + +Describes settings of floating windows. + +- Example: + + ```toml + [float] + show-pin-icon = true + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `show-pin-icon` (optional): + + Sets whether floating windows always show a pin icon. + + The default is `false`. + + The value of this field should be a boolean. + + ### `Format` @@ -2905,6 +2943,21 @@ The string should have one of the following values: Toggles floating windows showing above fullscreen windows. +- `pin-float`: + + Pins the currently focused floating window. + + If a floating window is pinned, it will stay visible even when switching to a + different workspace. + +- `unpin-float`: + + Unpins the currently focused floating window. + +- `toggle-float-pinned`: + + Toggles whether the currently focused floating window is pinned. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index a11fe4f4..d6b8ebf5 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -712,6 +712,18 @@ SimpleActionName: - value: toggle-float-above-fullscreen description: | Toggles floating windows showing above fullscreen windows. + - value: pin-float + description: | + Pins the currently focused floating window. + + If a floating window is pinned, it will stay visible even when switching to a + different workspace. + - value: unpin-float + description: | + Unpins the currently focused floating window. + - value: toggle-float-pinned + description: | + Toggles whether the currently focused floating window is pinned. Color: @@ -2326,6 +2338,18 @@ Config: [color-management] enabled = true ``` + float: + ref: Float + required: false + description: | + Configures the settings of floating windows. + + - Example: + + ```toml + [float] + show-pin-icon = true + ``` Idle: @@ -2834,3 +2858,24 @@ Brightness: - kind: number description: | The brightness in cd/m^2. + + +Float: + kind: table + description: | + Describes settings of floating windows. + + - Example: + + ```toml + [float] + show-pin-icon = true + ``` + fields: + show-pin-icon: + description: | + Sets whether floating windows always show a pin icon. + + The default is `false`. + kind: boolean + required: false