1
0
Fork 0
forked from wry/wry

tree: allow floats to be pinned

This commit is contained in:
Julian Orth 2025-04-24 14:21:25 +02:00
parent 3e6640f0ca
commit 65a66c2e26
28 changed files with 528 additions and 36 deletions

View file

@ -778,6 +778,20 @@ impl Client {
above 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 { pub fn connector_connected(&self, connector: Connector) -> bool {
let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector }); let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector });
get_response!(res, false, ConnectorConnected { connected }); get_response!(res, false, ConnectorConnected { connected });

View file

@ -546,6 +546,16 @@ pub enum ClientMessage<'a> {
above: bool, above: bool,
}, },
GetFloatAboveFullscreen, GetFloatAboveFullscreen,
GetFloatPinned {
seat: Seat,
},
SetFloatPinned {
seat: Seat,
pinned: bool,
},
SetShowFloatPinIcon {
show: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -697,6 +707,9 @@ pub enum Response {
GetFloatAboveFullscreen { GetFloatAboveFullscreen {
above: bool, above: bool,
}, },
GetFloatPinned {
pinned: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -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. /// A focus-follows-mouse mode.

View file

@ -43,6 +43,8 @@
)] )]
#![warn(unsafe_op_in_unsafe_fn)] #![warn(unsafe_op_in_unsafe_fn)]
#[expect(unused_imports)]
use crate::input::Seat;
use { use {
crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector}, crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector},
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
@ -292,3 +294,13 @@ pub fn get_float_above_fullscreen() -> bool {
pub fn toggle_float_above_fullscreen() { pub fn toggle_float_above_fullscreen() {
set_float_above_fullscreen(!get_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);
}

View file

@ -5,6 +5,8 @@
by using the `enable-float-above-fullscreen` action. by using the `enable-float-above-fullscreen` action.
- Implement xdg-toplevel-tag-v1. - Implement xdg-toplevel-tag-v1.
- Implement tablet-v2 version 2. - 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) # 1.10.0 (2025-04-22)

View file

@ -289,6 +289,7 @@ fn start_compositor2(
color_manager, color_manager,
float_above_fullscreen: Cell::new(false), float_above_fullscreen: Cell::new(false),
icons: Default::default(), icons: Default::default(),
show_pin_icon: Cell::new(false),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);
@ -618,6 +619,7 @@ fn create_dummy_output(state: &Rc<State>) {
tray_start_rel: Default::default(), tray_start_rel: Default::default(),
tray_items: Default::default(), tray_items: Default::default(),
ext_workspace_groups: Default::default(), ext_workspace_groups: Default::default(),
pinned: Default::default(),
}); });
let dummy_workspace = Rc::new(WorkspaceNode { let dummy_workspace = Rc::new(WorkspaceNode {
id: state.node_ids.next(), id: state.node_ids.next(),

View file

@ -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( fn handle_set_vrr_mode(
&self, &self,
connector: Option<Connector>, connector: Option<Connector>,
@ -2060,6 +2083,15 @@ impl ConfigProxyHandler {
self.handle_set_float_above_fullscreen(above) self.handle_set_float_above_fullscreen(above)
} }
ClientMessage::GetFloatAboveFullscreen => self.handle_get_float_above_fullscreen(), 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(()) Ok(())
} }

View file

@ -28,7 +28,6 @@ pub enum IconState {
Passive, Passive,
} }
#[expect(dead_code)]
pub struct SizedIcons { pub struct SizedIcons {
pub pin_unfocused_title: StaticMap<IconState, Rc<dyn GfxTexture>>, pub pin_unfocused_title: StaticMap<IconState, Rc<dyn GfxTexture>>,
pub pin_focused_title: StaticMap<IconState, Rc<dyn GfxTexture>>, pub pin_focused_title: StaticMap<IconState, Rc<dyn GfxTexture>>,

View file

@ -1119,6 +1119,20 @@ impl WlSeatGlobal {
}; };
kb.phy_state.destroy(self.state.now_usec(), self); 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 { impl CursorUserOwner for WlSeatGlobal {

View file

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect}, gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect},
icons::SizedIcons, icons::{IconState, SizedIcons},
ifs::wl_surface::{ ifs::wl_surface::{
SurfaceBuffer, WlSurface, SurfaceBuffer, WlSurface,
x_surface::xwindow::Xwindow, x_surface::xwindow::Xwindow,
@ -28,7 +28,6 @@ pub struct Renderer<'a> {
pub state: &'a State, pub state: &'a State,
pub logical_extents: Rect, pub logical_extents: Rect,
pub pixel_extents: Rect, pub pixel_extents: Rect,
#[expect(dead_code)]
pub icons: Option<Rc<SizedIcons>>, pub icons: Option<Rc<SizedIcons>>,
} }
@ -524,11 +523,45 @@ impl Renderer<'_> {
let title_underline = let title_underline =
[Rect::new_sized(x + bw, y + bw + th, pos.width() - 2 * bw, 1).unwrap()]; [Rect::new_sized(x + bw, y + bw + th, pos.width() - 2 * bw, 1).unwrap()];
self.base.fill_boxes(&title_underline, &uc, srgb); 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(title) = floating.title_textures.borrow().get(&self.base.scale) {
if let Some(texture) = title.texture() { if let Some(texture) = title.texture() {
let rect = floating.title_rect.get().move_(x, y); let (x, y) = self.base.scale_point(x1, y1);
let bounds = self.base.scale_rect(rect);
let (x, y) = self.base.scale_point(rect.x1(), rect.y1());
self.base.render_texture( self.base.render_texture(
&texture, &texture,
None, None,

View file

@ -81,7 +81,7 @@ use {
tree::{ tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node, ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node,
NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode,
ToplevelNodeBase, VrrMode, WorkspaceNode, ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor,
}, },
utils::{ utils::{
activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings, activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings,
@ -115,7 +115,7 @@ use {
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::{Debug, Formatter}, fmt::{Debug, Formatter},
mem, mem,
ops::DerefMut, ops::{Deref, DerefMut},
rc::{Rc, Weak}, rc::{Rc, Weak},
sync::Arc, sync::Arc,
time::Duration, time::Duration,
@ -239,6 +239,7 @@ pub struct State {
pub color_manager: Rc<ColorManager>, pub color_manager: Rc<ColorManager>,
pub float_above_fullscreen: Cell<bool>, pub float_above_fullscreen: Cell<bool>,
pub icons: Icons, pub icons: Icons,
pub show_pin_icon: Cell<bool>,
} }
// impl Drop for State { // impl Drop for State {
@ -742,8 +743,21 @@ impl State {
let (output, ws) = match self.workspaces.get(name) { let (output, ws) = match self.workspaces.get(name) {
Some(ws) => { Some(ws) => {
let output = ws.output.get(); 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); 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 { if !did_change {
return; return;
} }

View file

@ -195,6 +195,7 @@ impl ConnectorHandler {
tray_start_rel: Default::default(), tray_start_rel: Default::default(),
tray_items: Default::default(), tray_items: Default::default(),
ext_workspace_groups: Default::default(), ext_workspace_groups: Default::default(),
pinned: Default::default(),
}); });
on.update_visible(); on.update_visible();
on.update_rects(); on.update_rects();

View file

@ -2041,6 +2041,14 @@ impl ContainingNode for ContainerNode {
} }
} }
} }
fn cnode_pinned(&self) -> bool {
self.tl_pinned()
}
fn cnode_set_pinned(self: Rc<Self>, pinned: bool) {
self.tl_set_pinned(false, pinned);
}
} }
impl ToplevelNodeBase for ContainerNode { impl ToplevelNodeBase for ContainerNode {

View file

@ -31,4 +31,10 @@ pub trait ContainingNode: Node {
let _ = new_y1; let _ = new_y1;
let _ = new_y2; let _ = new_y2;
} }
fn cnode_pinned(&self) -> bool {
false
}
fn cnode_set_pinned(self: Rc<Self>, pinned: bool) {
let _ = pinned;
}
} }

View file

@ -5,7 +5,7 @@ use {
cursor_user::CursorUser, cursor_user::CursorUser,
fixed::Fixed, fixed::Fixed,
ifs::wl_seat::{ ifs::wl_seat::{
BTN_LEFT, NodeSeatState, SeatId, WlSeatGlobal, BTN_LEFT, BTN_RIGHT, NodeSeatState, SeatId, WlSeatGlobal,
tablet::{TabletTool, TabletToolChanges, TabletToolId}, tablet::{TabletTool, TabletToolChanges, TabletToolId},
}, },
rect::Rect, rect::Rect,
@ -15,7 +15,7 @@ use {
text::TextTexture, text::TextTexture,
tree::{ tree::{
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId,
OutputNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode,
walker::NodeVisitor, walker::NodeVisitor,
}, },
utils::{ utils::{
@ -25,6 +25,7 @@ use {
}, },
}, },
ahash::AHashMap, ahash::AHashMap,
arrayvec::ArrayVec,
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
fmt::{Debug, Formatter}, fmt::{Debug, Formatter},
@ -42,6 +43,7 @@ pub struct FloatNode {
pub position: Cell<Rect>, pub position: Cell<Rect>,
pub display_link: RefCell<Option<LinkedNode<Rc<dyn StackedNode>>>>, pub display_link: RefCell<Option<LinkedNode<Rc<dyn StackedNode>>>>,
pub workspace_link: Cell<Option<LinkedNode<Rc<dyn StackedNode>>>>, pub workspace_link: Cell<Option<LinkedNode<Rc<dyn StackedNode>>>>,
pub pinned_link: RefCell<Option<LinkedNode<Rc<dyn PinnedNode>>>>,
pub workspace: CloneCell<Rc<WorkspaceNode>>, pub workspace: CloneCell<Rc<WorkspaceNode>>,
pub child: CloneCell<Option<Rc<dyn ToplevelNode>>>, pub child: CloneCell<Option<Rc<dyn ToplevelNode>>>,
pub active: Cell<bool>, pub active: Cell<bool>,
@ -120,6 +122,7 @@ impl FloatNode {
position: Cell::new(position), position: Cell::new(position),
display_link: RefCell::new(None), display_link: RefCell::new(None),
workspace_link: Cell::new(None), workspace_link: Cell::new(None),
pinned_link: RefCell::new(None),
workspace: CloneCell::new(ws.clone()), workspace: CloneCell::new(ws.clone()),
child: CloneCell::new(Some(child.clone())), child: CloneCell::new(Some(child.clone())),
active: Cell::new(false), active: Cell::new(false),
@ -144,6 +147,9 @@ impl FloatNode {
if floater.visible.get() { if floater.visible.get() {
state.damage(position); state.damage(position);
} }
if child.tl_data().pinned.get() {
floater.toggle_pinned();
}
floater floater
} }
@ -217,6 +223,9 @@ impl FloatNode {
let mut th = tr.height(); let mut th = tr.height();
let mut scalef = None; let mut scalef = None;
let mut width = tr.width(); 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 { if *scale != 1 {
let scale = scale.to_f64(); let scale = scale.to_f64();
th = (th as f64 * scale).round() as _; th = (th as f64 * scale).round() as _;
@ -402,17 +411,32 @@ impl FloatNode {
} }
} }
fn set_workspace(self: &Rc<Self>, ws: &Rc<WorkspaceNode>) { fn set_workspace_(
self: &Rc<Self>,
ws: &Rc<WorkspaceNode>,
update_pinned: bool,
update_visible: bool,
) {
if let Some(c) = self.child.get() { if let Some(c) = self.child.get() {
c.tl_set_workspace(ws); c.tl_set_workspace(ws);
} }
self.workspace_link self.workspace_link
.set(Some(ws.stacked.add_last(self.clone()))); .set(Some(ws.stacked.add_last(self.clone())));
self.workspace.set(ws.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<Self>, output: &Rc<OutputNode>) { pub fn after_ws_move(self: &Rc<Self>, output: &Rc<OutputNode>) {
if let Some(pinned) = &*self.pinned_link.borrow() {
output.pinned.add_last_existing(pinned);
}
if output.is_dummy { if output.is_dummy {
return; return;
} }
@ -505,6 +529,20 @@ impl FloatNode {
} }
} }
fn toggle_pinned(self: &Rc<Self>) {
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( fn button(
self: Rc<Self>, self: Rc<Self>,
id: CursorType, id: CursorType,
@ -518,6 +556,34 @@ impl FloatNode {
Some(s) => s, Some(s) => s,
_ => return, _ => 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::<FloatIcon, 1>::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 !cursor_data.op_active {
if !pressed { if !pressed {
return; return;
@ -533,6 +599,7 @@ impl FloatNode {
cursor_data.x, cursor_data.x,
cursor_data.y, cursor_data.y,
) && cursor_data.op_type == OpType::Move ) && cursor_data.op_type == OpType::Move
&& !is_icon_press
{ {
if let Some(tl) = self.child.get() { if let Some(tl) = self.child.get() {
drop(cursors); drop(cursors);
@ -572,7 +639,7 @@ impl FloatNode {
} else if !pressed { } else if !pressed {
cursor_data.op_active = false; cursor_data.op_active = false;
let ws = cursor.output().ensure_workspace(); 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, state: KeyState,
_serial: u64, _serial: u64,
) { ) {
if button == BTN_RIGHT && state == KeyState::Pressed {
self.toggle_pinned();
}
if button != BTN_LEFT { if button != BTN_LEFT {
return; return;
} }
@ -800,6 +870,7 @@ impl ContainingNode for FloatNode {
self.child.set(None); self.child.set(None);
self.display_link.borrow_mut().take(); self.display_link.borrow_mut().take();
self.workspace_link.set(None); self.workspace_link.set(None);
self.pinned_link.take();
if self.visible.get() { if self.visible.get() {
self.state.damage(self.position.get()); self.state.damage(self.position.get());
} }
@ -874,6 +945,17 @@ impl ContainingNode for FloatNode {
self.schedule_layout(); self.schedule_layout();
} }
} }
fn cnode_pinned(&self) -> bool {
self.pinned_link.borrow().is_some()
}
fn cnode_set_pinned(self: Rc<Self>, pinned: bool) {
if self.pinned_link.borrow().is_some() == pinned {
return;
}
self.toggle_pinned();
}
} }
impl StackedNode for FloatNode { impl StackedNode for FloatNode {
@ -891,3 +973,9 @@ impl StackedNode for FloatNode {
true true
} }
} }
impl PinnedNode for FloatNode {
fn set_workspace(self: Rc<Self>, workspace: &Rc<WorkspaceNode>, update_visible: bool) {
self.set_workspace_(workspace, false, update_visible);
}
}

View file

@ -39,9 +39,9 @@ use {
state::State, state::State,
text::TextTexture, text::TextTexture,
tree::{ tree::{
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, StackedNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, PinnedNode,
TddType, TileDragDestination, WorkspaceDragDestination, WorkspaceNode, WorkspaceNodeId, StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, WorkspaceNode,
walker::NodeVisitor, WorkspaceNodeId, walker::NodeVisitor,
}, },
utils::{ utils::{
asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap, asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap,
@ -103,6 +103,7 @@ pub struct OutputNode {
pub tray_start_rel: Cell<i32>, pub tray_start_rel: Cell<i32>,
pub tray_items: LinkedList<Rc<dyn DynTrayItem>>, pub tray_items: LinkedList<Rc<dyn DynTrayItem>>,
pub ext_workspace_groups: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceGroupHandleV1>>, pub ext_workspace_groups: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceGroupHandleV1>>,
pub pinned: LinkedList<Rc<dyn PinnedNode>>,
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@ -646,6 +647,9 @@ impl OutputNode {
return false; return false;
} }
collect_kb_foci2(old.clone(), &mut seats); collect_kb_foci2(old.clone(), &mut seats);
for pinned in self.pinned.iter() {
pinned.deref().clone().set_workspace(ws, false);
}
if old.is_empty() { if old.is_empty() {
for jw in old.jay_workspaces.lock().values() { for jw in old.jay_workspaces.lock().values() {
jw.send_destroyed(); jw.send_destroyed();

View file

@ -1,4 +1,7 @@
use crate::tree::Node; use {
crate::tree::{Node, WorkspaceNode},
std::rc::Rc,
};
pub trait StackedNode: Node { pub trait StackedNode: Node {
fn stacked_prepare_set_visible(&self) { fn stacked_prepare_set_visible(&self) {
@ -14,3 +17,7 @@ pub trait StackedNode: Node {
true true
} }
} }
pub trait PinnedNode: StackedNode {
fn set_workspace(self: Rc<Self>, workspace: &Rc<WorkspaceNode>, update_visible: bool);
}

View file

@ -50,6 +50,8 @@ pub trait ToplevelNode: ToplevelNodeBase {
fn tl_change_extents(self: Rc<Self>, rect: &Rect); fn tl_change_extents(self: Rc<Self>, rect: &Rect);
fn tl_set_visible(&self, visible: bool); fn tl_set_visible(&self, visible: bool);
fn tl_destroy(&self); fn tl_destroy(&self);
fn tl_pinned(&self) -> bool;
fn tl_set_pinned(&self, self_pinned: bool, pinned: bool);
} }
impl<T: ToplevelNodeBase> ToplevelNode for T { impl<T: ToplevelNodeBase> ToplevelNode for T {
@ -151,6 +153,24 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
self.tl_data().destroy_node(self); self.tl_data().destroy_node(self);
self.tl_destroy_impl(); 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 { pub trait ToplevelNodeBase: Node {
@ -243,6 +263,7 @@ pub struct ToplevelData {
pub is_floating: Cell<bool>, pub is_floating: Cell<bool>,
pub float_width: Cell<i32>, pub float_width: Cell<i32>,
pub float_height: Cell<i32>, pub float_height: Cell<i32>,
pub pinned: Cell<bool>,
pub is_fullscreen: Cell<bool>, pub is_fullscreen: Cell<bool>,
pub fullscrceen_data: RefCell<Option<FullscreenedData>>, pub fullscrceen_data: RefCell<Option<FullscreenedData>>,
pub workspace: CloneCell<Option<Rc<WorkspaceNode>>>, pub workspace: CloneCell<Option<Rc<WorkspaceNode>>>,
@ -283,6 +304,7 @@ impl ToplevelData {
is_floating: Default::default(), is_floating: Default::default(),
float_width: Default::default(), float_width: Default::default(),
float_height: Default::default(), float_height: Default::default(),
pinned: Cell::new(false),
is_fullscreen: Default::default(), is_fullscreen: Default::default(),
fullscrceen_data: Default::default(), fullscrceen_data: Default::default(),
workspace: Default::default(), workspace: Default::default(),

View file

@ -128,7 +128,7 @@ impl WorkspaceNode {
} }
fn visit_float(&mut self, node: &Rc<FloatNode>) { fn visit_float(&mut self, node: &Rc<FloatNode>) {
node.adjust_position_after_ws_move(self.0); node.after_ws_move(self.0);
node.node_visit_children(self); node.node_visit_children(self);
} }
@ -426,6 +426,27 @@ pub fn move_ws_to_output(
config: WsMoveConfig, config: WsMoveConfig,
) { ) {
let source = ws.output.get(); 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); ws.set_output(&target);
'link: { 'link: {
if let Some(before) = config.before { if let Some(before) = config.before {
@ -445,18 +466,9 @@ pub fn move_ws_to_output(
ws.set_visible(false); ws.set_visible(false);
} }
ws.flush_jay_workspaces(); ws.flush_jay_workspaces();
if let Some(visible) = source.workspace.get() { if let Some(ws) = new_source_ws {
if visible.id == ws.id { source.show_workspace(&ws);
source.workspace.take(); ws.flush_jay_workspaces();
}
}
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 !target.is_dummy { if !target.is_dummy {
target.schedule_update_render_data(); target.schedule_update_render_data();

View file

@ -14,6 +14,7 @@ use {
parsers::{ parsers::{
color_management::ColorManagement, color_management::ColorManagement,
config::{ConfigParser, ConfigParserError}, config::{ConfigParser, ConfigParserError},
float::Float,
}, },
}, },
toml::{self}, toml::{self},
@ -58,6 +59,8 @@ pub enum SimpleCommand {
EnableWindowManagement(bool), EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool), SetFloatAboveFullscreen(bool),
ToggleFloatAboveFullscreen, ToggleFloatAboveFullscreen,
SetFloatPinned(bool),
ToggleFloatPinned,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -367,6 +370,7 @@ pub struct Config {
pub ui_drag: UiDrag, pub ui_drag: UiDrag,
pub xwayland: Option<Xwayland>, pub xwayland: Option<Xwayland>,
pub color_management: Option<ColorManagement>, pub color_management: Option<ColorManagement>,
pub float: Option<Float>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -16,6 +16,7 @@ mod drm_device;
mod drm_device_match; mod drm_device_match;
mod env; mod env;
pub mod exec; pub mod exec;
pub mod float;
mod format; mod format;
mod gfx_api; mod gfx_api;
mod idle; mod idle;

View file

@ -116,6 +116,9 @@ impl ActionParser<'_> {
"enable-float-above-fullscreen" => SetFloatAboveFullscreen(true), "enable-float-above-fullscreen" => SetFloatAboveFullscreen(true),
"disable-float-above-fullscreen" => SetFloatAboveFullscreen(false), "disable-float-above-fullscreen" => SetFloatAboveFullscreen(false),
"toggle-float-above-fullscreen" => ToggleFloatAboveFullscreen, "toggle-float-above-fullscreen" => ToggleFloatAboveFullscreen,
"pin-float" => SetFloatPinned(true),
"unpin-float" => SetFloatPinned(false),
"toggle-float-pinned" => ToggleFloatPinned,
_ => { _ => {
return Err( return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -12,6 +12,7 @@ use {
drm_device::DrmDevicesParser, drm_device::DrmDevicesParser,
drm_device_match::DrmDeviceMatchParser, drm_device_match::DrmDeviceMatchParser,
env::EnvParser, env::EnvParser,
float::FloatParser,
gfx_api::GfxApiParser, gfx_api::GfxApiParser,
idle::IdleParser, idle::IdleParser,
input::InputsParser, input::InputsParser,
@ -118,7 +119,7 @@ impl Parser for ConfigParser<'_> {
ui_drag_val, ui_drag_val,
xwayland_val, xwayland_val,
), ),
(color_management_val,), (color_management_val, float_val),
) = ext.extract(( ) = ext.extract((
( (
opt(val("keymap")), opt(val("keymap")),
@ -156,7 +157,7 @@ impl Parser for ConfigParser<'_> {
opt(val("ui-drag")), opt(val("ui-drag")),
opt(val("xwayland")), opt(val("xwayland")),
), ),
(opt(val("color-management")),), (opt(val("color-management")), opt(val("float"))),
))?; ))?;
let mut keymap = None; let mut keymap = None;
if let Some(value) = keymap_val { 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 { Ok(Config {
keymap, keymap,
repeat_rate, repeat_rate,
@ -412,6 +422,7 @@ impl Parser for ConfigParser<'_> {
ui_drag, ui_drag,
xwayland, xwayland,
color_management, color_management,
float,
}) })
} }
} }

View file

@ -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<bool>,
}
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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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(),
})
}
}

View file

@ -25,7 +25,8 @@ use {
logging::set_log_level, logging::set_log_level,
on_devices_enumerated, on_idle, quit, reload, set_color_management_enabled, on_devices_enumerated, on_idle, quit, reload, set_color_management_enabled,
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, 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}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
theme::{reset_colors, reset_font, reset_sizes, set_font}, theme::{reset_colors, reset_font, reset_sizes, set_font},
@ -101,6 +102,8 @@ impl Action {
B::new(move || set_float_above_fullscreen(bool)) B::new(move || set_float_above_fullscreen(bool))
} }
SimpleCommand::ToggleFloatAboveFullscreen => B::new(toggle_float_above_fullscreen), 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 } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); 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<PersistentState>) {
set_color_management_enabled(enabled); 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 { fn create_command(exec: &Exec) -> Command {

View file

@ -636,6 +636,10 @@
"color-management": { "color-management": {
"description": "Configures the color-management settings.\n\n- Example:\n\n ```toml\n [color-management]\n enabled = true\n ```\n", "description": "Configures the color-management settings.\n\n- Example:\n\n ```toml\n [color-management]\n enabled = true\n ```\n",
"$ref": "#/$defs/ColorManagement" "$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": [] "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": { "Format": {
"type": "string", "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", "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", "disable-window-management",
"enable-float-above-fullscreen", "enable-float-above-fullscreen",
"disable-float-above-fullscreen", "disable-float-above-fullscreen",
"toggle-float-above-fullscreen" "toggle-float-above-fullscreen",
"pin-float",
"unpin-float",
"toggle-float-pinned"
] ]
}, },
"Status": { "Status": {

View file

@ -1265,6 +1265,19 @@ The table has the following fields:
The value of this field should be a [ColorManagement](#types-ColorManagement). 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).
<a name="types-Connector"></a> <a name="types-Connector"></a>
### `Connector` ### `Connector`
@ -1629,6 +1642,31 @@ The table has the following fields:
The value of this field should be a boolean. The value of this field should be a boolean.
<a name="types-Float"></a>
### `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.
<a name="types-Format"></a> <a name="types-Format"></a>
### `Format` ### `Format`
@ -2905,6 +2943,21 @@ The string should have one of the following values:
Toggles floating windows showing above fullscreen windows. 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.
<a name="types-Status"></a> <a name="types-Status"></a>

View file

@ -712,6 +712,18 @@ SimpleActionName:
- value: toggle-float-above-fullscreen - value: toggle-float-above-fullscreen
description: | description: |
Toggles floating windows showing above fullscreen windows. 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: Color:
@ -2326,6 +2338,18 @@ Config:
[color-management] [color-management]
enabled = true enabled = true
``` ```
float:
ref: Float
required: false
description: |
Configures the settings of floating windows.
- Example:
```toml
[float]
show-pin-icon = true
```
Idle: Idle:
@ -2834,3 +2858,24 @@ Brightness:
- kind: number - kind: number
description: | description: |
The brightness in cd/m^2. 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