diff --git a/Cargo.lock b/Cargo.lock index d53d97ff..1b7a9e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -183,6 +189,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" + [[package]] name = "byteorder" version = "1.5.0" @@ -597,6 +609,7 @@ dependencies = [ "shaderc", "smallvec", "thiserror", + "tiny-skia", "tracy-client-sys", "uapi", ] @@ -1245,6 +1258,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.11.1" @@ -1345,6 +1364,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 86156da9..dd43f8d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ png = "0.17.13" rustc-demangle = { version = "0.1.24", optional = true } tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } kbvm = "0.1.4" +tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } [build-dependencies] repc = "0.1.1" 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 82b5f8bb..e94cdc05 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -288,6 +288,8 @@ fn start_compositor2( color_management_enabled: Cell::new(false), 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); @@ -617,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 847a9fdc..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, @@ -1545,6 +1568,7 @@ impl ConfigProxyHandler { } self.state.root.clone().node_visit(&mut V); self.state.damage(self.state.root.extents.get()); + self.state.icons.update_sizes(&self.state); } fn colors_changed(&self) { @@ -1561,6 +1585,7 @@ impl ConfigProxyHandler { } self.state.root.clone().node_visit(&mut V); self.state.damage(self.state.root.extents.get()); + self.state.icons.clear(); } fn get_sized(&self, sized: Resizable) -> Result { @@ -2058,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/gfx_api.rs b/src/gfx_api.rs index e442d4a0..303b63e9 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -618,6 +618,7 @@ impl dyn GfxFramebuffer { let (width, height) = self.logical_size(transform); Rect::new(0, 0, width, height).unwrap() }, + icons: None, }; cursor.render_hardware_cursor(&mut renderer); self.render( @@ -906,6 +907,7 @@ pub fn create_render_pass( let (width, height) = logical_size(physical_size, transform); Rect::new(0, 0, width, height).unwrap() }, + icons: state.icons.get(state, scale), }; node.node_render(&mut renderer, 0, 0, None); if let Some(rect) = cursor_rect { diff --git a/src/icons.rs b/src/icons.rs new file mode 100644 index 00000000..ba916498 --- /dev/null +++ b/src/icons.rs @@ -0,0 +1,283 @@ +#![allow(clippy::excessive_precision)] + +use { + crate::{ + cmm::cmm_transfer_function::TransferFunction, + format::ARGB8888, + gfx_api::{GfxContext, GfxError, GfxTexture}, + scale::Scale, + state::State, + theme::Theme, + utils::{copyhashmap::CopyHashMap, windows::WindowsExt}, + }, + ahash::AHashSet, + linearize::{Linearize, StaticMap, static_map}, + std::{cell::Cell, f32::consts::PI, mem, rc::Rc, sync::LazyLock}, + thiserror::Error, + tiny_skia::{Color, FillRule, Paint, Path, PathBuilder, Pixmap, Transform}, +}; + +#[derive(Default)] +pub struct Icons { + icons: CopyHashMap>>, +} + +#[derive(Copy, Clone, Debug, Linearize)] +pub enum IconState { + Active, + Passive, +} + +pub struct SizedIcons { + pub pin_unfocused_title: StaticMap>, + pub pin_focused_title: StaticMap>, + pub pin_attention_requested: StaticMap>, +} + +#[derive(Debug, Error)] +pub enum IconsError { + #[error("Could not create a pixmap")] + CreatePixmap, + #[error("The requested icons size is non-positive")] + NonPositiveSize, + #[error("There is no gfx context")] + NoRenderContext, + #[error("Could not create texture")] + CreateTexture(#[source] GfxError), +} + +impl Icons { + pub fn update_sizes(&self, state: &State) { + let mut sizes = AHashSet::new(); + let height = state.theme.sizes.title_height.get(); + for &(scale, _) in &*state.scales.lock() { + let [size] = scale.pixel_size([height]); + if size > 0 { + sizes.insert(size); + } + } + self.icons.lock().retain(|size, _| sizes.contains(size)); + } + + pub fn clear(&self) { + self.icons.clear(); + } + + pub fn get(&self, state: &State, scale: Scale) -> Option> { + let [size] = scale.pixel_size([state.theme.sizes.title_height.get()]); + if let Some(icons) = self.icons.get(&size) { + return icons; + } + let icons = match self.create(state, size) { + Ok(i) => Some(i), + Err(e) => { + log::error!("Could not create icons: {}", e); + None + } + }; + self.icons.set(size, icons.clone()); + icons + } + + fn create(&self, state: &State, size: i32) -> Result, IconsError> { + let Some(ctx) = state.render_ctx.get() else { + return Err(IconsError::NoRenderContext); + }; + Ok(Rc::new(create_icons(size, &state.theme, &ctx)?)) + } +} + +pub fn create_icons( + size: i32, + theme: &Theme, + ctx: &Rc, +) -> Result { + if size <= 0 { + return Err(IconsError::NonPositiveSize); + } + let size = size as u32; + + let create_pins = |color: crate::theme::Color| { + let create_pin = |color: Color| { + let mut paint = Paint::default(); + paint.set_color(color); + let s = size as f32 / 100.0; + let transform = Transform::from_scale(s, s); + let mut pixmap = Pixmap::new(size, size).ok_or(IconsError::CreatePixmap)?; + pixmap.fill_path(&PIN_PATH, &paint, FillRule::EvenOdd, transform, None); + upload_pixmap(pixmap, ctx) + }; + let colors = calculate_accents(color); + Ok(static_map! { + IconState::Passive => create_pin(colors[0])?, + IconState::Active => create_pin(colors[1])?, + }) + }; + + Ok(SizedIcons { + pin_unfocused_title: create_pins(theme.colors.unfocused_title_background.get())?, + pin_focused_title: create_pins(theme.colors.focused_title_background.get())?, + pin_attention_requested: create_pins(theme.colors.attention_requested_background.get())?, + }) +} + +fn upload_pixmap( + pixmap: Pixmap, + ctx: &Rc, +) -> Result, IconsError> { + let width = pixmap.width(); + let height = pixmap.width(); + let bytes = unsafe { mem::transmute::, Vec>>(pixmap.take()) }; + for chunk in bytes.array_chunks_ext::<4>() { + let r = chunk[0].get(); + let b = chunk[2].get(); + chunk[0].set(b); + chunk[2].set(r); + } + let tex: Rc = ctx + .clone() + .shmem_texture( + None, + &bytes, + ARGB8888, + width as _, + height as _, + width as i32 * 4, + None, + ) + .map_err(IconsError::CreateTexture)?; + Ok(tex) +} + +static PIN_PATH: LazyLock = LazyLock::new(|| { + let cx = 50.0f32; + let cy = 40.0f32; + let r = 30.0f32; + let xx = cx; + let xy = 90.0f32; + let d = xy - cy; + let v1 = r / d * (d * d - r * r).sqrt(); + let v2 = 1.0 / d * (d * d - r * r); + + let mut path = PathBuilder::new(); + path.move_to(cx, cy - r); + path.arc_cw_to(cx, cy, cx + r, cy); + path.arc_cw_to(cx, cy, xx + v1, xy - v2); + path.line_to(xx, xy); + path.line_to(xx - v1, xy - v2); + path.arc_cw_to(cx, cy, cx - r, cy); + path.arc_cw_to(cx, cy, cx, cy - r); + path.close(); + path.push_circle(cx, cy, r / 2.5); + path.finish().unwrap() +}); + +#[test] +fn pin_path() { + let _path = &*PIN_PATH; +} + +trait PathBuilderExt { + fn arc_cw_to(&mut self, cx: f32, cy: f32, x: f32, y: f32); +} + +impl PathBuilderExt for PathBuilder { + fn arc_cw_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + let (x0, y0) = match self.last_point() { + None => { + self.move_to(0.0, 0.0); + (0.0, 0.0) + } + Some(p) => (p.x, p.y), + }; + + let ux = x0 - cx; + let uy = y0 - cy; + let ul = (ux * ux + uy * uy).sqrt(); + let uxn = ux / ul; + let uyn = uy / ul; + let a1 = (uy / ux).atan(); + + let tx = x - cx; + let ty = y - cy; + let tl = (tx * tx + ty * ty).sqrt(); + let txn = tx / tl; + let tyn = ty / tl; + let a2 = (ty / tx).atan(); + + let c = 4.0 / 3.0 * ((a2 - a1 + PI) % PI / 4.0).tan(); + let uc = ul * c; + let tc = tl * c; + self.cubic_to( + x0 - uyn * uc, + y0 + uxn * uc, + x + tyn * tc, + y - txn * tc, + x, + y, + ); + } +} + +impl From for Color { + fn from(v: crate::theme::Color) -> Self { + let [r, g, b, a] = v.to_array(TransferFunction::Srgb); + let mut c = Self::TRANSPARENT; + c.set_red(r / a); + c.set_green(g / a); + c.set_blue(b / a); + c.set_alpha(a); + c + } +} + +fn calculate_accents(srgb: crate::theme::Color) -> [Color; 2] { + let [l, a, b, alpha] = srgb_to_lab(srgb); + let l2 = if l < 0.65 { 0.9 } else { l - 0.4 }; + let l1 = (l2 + l) / 2.0; + [ + lab_to_color([l1, a, b, alpha]), + lab_to_color([l2, a, b, alpha]), + ] +} + +fn srgb_to_lab(srgb: crate::theme::Color) -> [f32; 4] { + let [mut r, mut g, mut b, alpha] = srgb.to_array(TransferFunction::Srgb); + if alpha < 1.0 { + r /= alpha; + g /= alpha; + b /= alpha; + } + + let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; + let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; + let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; + + let l_ = l.cbrt(); + let m_ = m.cbrt(); + let s_ = s.cbrt(); + + [ + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + alpha, + ] +} + +fn lab_to_color([l, a, b, alpha]: [f32; 4]) -> Color { + let l_ = l + 0.3963377774 * a + 0.2158037573 * b; + let m_ = l - 0.1055613458 * a - 0.0638541728 * b; + let s_ = l - 0.0894841775 * a - 1.2914855480 * b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + let mut c = Color::TRANSPARENT; + c.set_red(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s); + c.set_green(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s); + c.set_blue(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s); + c.set_alpha(alpha); + c +} 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/main.rs b/src/main.rs index 6c9052e4..974d0441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ mod format; mod gfx_api; mod gfx_apis; mod globals; +mod icons; mod ifs; mod io_uring; #[cfg(feature = "it")] diff --git a/src/renderer.rs b/src/renderer.rs index 16841c33..d49d0032 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,7 @@ use { crate::{ gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect}, + icons::{IconState, SizedIcons}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -27,6 +28,7 @@ pub struct Renderer<'a> { pub state: &'a State, pub logical_extents: Rect, pub pixel_extents: Rect, + pub icons: Option>, } impl Renderer<'_> { @@ -521,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 b488ac28..bc972c53 100644 --- a/src/state.rs +++ b/src/state.rs @@ -33,6 +33,7 @@ use { }, gfx_apis::create_gfx_context, globals::{Globals, GlobalsError, RemovableWaylandGlobal, WaylandGlobal}, + icons::Icons, ifs::{ ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, ext_idle_notification_v1::ExtIdleNotificationV1, @@ -80,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, @@ -114,7 +115,7 @@ use { cell::{Cell, RefCell}, fmt::{Debug, Formatter}, mem, - ops::DerefMut, + ops::{Deref, DerefMut}, rc::{Rc, Weak}, sync::Arc, time::Duration, @@ -237,6 +238,8 @@ pub struct State { pub color_management_enabled: Cell, pub color_manager: Rc, pub float_above_fullscreen: Cell, + pub icons: Icons, + pub show_pin_icon: Cell, } // impl Drop for State { @@ -466,6 +469,7 @@ impl State { UpdateTextTexturesVisitor.visit_display(&self.root); self.reload_cursors(); self.update_xwayland_wire_scale(); + self.icons.update_sizes(self); } fn cursor_sizes_changed(&self) { @@ -501,6 +505,7 @@ impl State { self.render_ctx_version.fetch_add(1); self.cursors.set(None); self.drm_feedback.set(None); + self.icons.clear(); self.wait_for_sync_obj .set_ctx(ctx.as_ref().and_then(|c| c.sync_obj_ctx().cloned())); @@ -738,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; } @@ -1044,6 +1062,7 @@ impl State { let (width, height) = target.logical_size(target_transform); Rect::new_sized(0, 0, width, height).unwrap() }, + icons: None, }; let mut sample_rect = SampleRect::identity(); sample_rect.buffer_transform = transform; 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/theme.rs b/src/theme.rs index 8d78d814..3fba81b1 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -182,7 +182,6 @@ impl Color { Self::new(TransferFunction::Srgb, to_f32(r), to_f32(g), to_f32(b)) } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self::new_premultiplied( TransferFunction::Srgb, @@ -220,7 +219,6 @@ impl Color { c } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn to_srgba_premultiplied(self) -> [u8; 4] { let [r, g, b, a] = self.to_array(TransferFunction::Srgb); [to_u8(r), to_u8(g), to_u8(b), to_u8(a)] @@ -332,7 +330,6 @@ impl Color { res } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn and_then(self, other: &Color) -> Color { Color { r: self.r * (1.0 - other.a) + other.r, 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 adab1bba..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,8 @@ use { text::TextTexture, tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, walker::NodeVisitor, + OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, + walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -24,6 +25,7 @@ use { }, }, ahash::AHashMap, + arrayvec::ArrayVec, std::{ cell::{Cell, RefCell}, fmt::{Debug, Formatter}, @@ -41,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, @@ -119,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), @@ -143,6 +147,9 @@ impl FloatNode { if floater.visible.get() { state.damage(position); } + if child.tl_data().pinned.get() { + floater.toggle_pinned(); + } floater } @@ -216,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 _; @@ -401,14 +411,72 @@ 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 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; + } + let pos = self.position.get(); + let opos = output.global.pos.get(); + if pos.intersects(&opos) { + return; + } + let bw = self.state.theme.sizes.border_width.get(); + let th = self.state.theme.sizes.title_height.get(); + let mut x1 = pos.x1(); + let mut x2 = pos.x2(); + let mut y1 = pos.y1(); + let mut y2 = pos.y2(); + const DELTA: i32 = 100; + let delta = bw + DELTA; + macro_rules! adjust { + ($z1:ident, $z2:ident) => { + if $z1 > opos.$z2() - delta { + $z1 = (opos.$z2() - delta).max(opos.$z1()); + $z2 += $z1 - pos.$z1(); + } else if $z2 < opos.$z1() + delta { + $z2 = (opos.$z1() + delta).min(opos.$z2()); + $z1 += $z2 - pos.$z2(); + } + }; + } + adjust!(x1, x2); + adjust!(y1, y2); + if y1 + bw + th <= opos.y1() { + y1 = opos.y1(); + y2 += y1 - pos.y1(); + } + let new_pos = Rect::new(x1, y1, x2, y2).unwrap(); + self.position.set(new_pos); + if self.visible.get() { + self.state.damage(pos); + self.state.damage(new_pos); + } + self.schedule_layout(); } fn update_child_title(self: &Rc, title: &str) { @@ -461,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, @@ -474,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; @@ -489,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); @@ -528,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); } } @@ -637,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; } @@ -756,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()); } @@ -830,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 { @@ -847,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 63d5b0d4..2f70be3a 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -20,8 +20,8 @@ use { state::State, text::TextTexture, tree::{ - ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - NodeVisitorBase, OutputNode, PlaceholderNode, StackedNode, ToplevelNode, + ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, + NodeId, NodeVisitorBase, OutputNode, PlaceholderNode, StackedNode, ToplevelNode, container::ContainerNode, walker::NodeVisitor, }, utils::{ @@ -127,6 +127,11 @@ impl WorkspaceNode { node.node_visit_children(self); } + fn visit_float(&mut self, node: &Rc) { + node.after_ws_move(self.0); + node.node_visit_children(self); + } + fn visit_xwindow(&mut self, node: &Rc) { node.tl_workspace_output_changed(); node.node_visit_children(self); @@ -421,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 { @@ -440,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