diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index 306b15ea..eea873df 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -253,6 +253,10 @@ pub mod colors { /// /// Default: `#772831`. const 13 => CAPTURED_FOCUSED_TITLE_BACKGROUND_COLOR, + /// The title background color of a window that has requested attention. + /// + /// Default: `#23092c`. + const 14 => ATTENTION_REQUESTED_BACKGROUND_COLOR, } } diff --git a/src/client.rs b/src/client.rs index 8999f11d..392b41c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,6 +7,7 @@ use { object::{Interface, Object, ObjectId, WL_DISPLAY_ID}, state::State, utils::{ + activation_token::ActivationToken, asyncevent::AsyncEvent, buffd::{MsgFormatter, MsgParser, MsgParserError, OutBufferSwapchain}, copyhashmap::{CopyHashMap, Locked}, @@ -147,6 +148,7 @@ impl Clients { symmetric_delete: Cell::new(false), last_xwayland_serial: Cell::new(0), surfaces_by_xwayland_serial: Default::default(), + activation_tokens: Default::default(), }); track!(data, data); let display = Rc::new(WlDisplay::new(&data)); @@ -217,6 +219,7 @@ impl Drop for ClientHolder { self.data.flush_request.clear(); self.data.shutdown.clear(); self.data.surfaces_by_xwayland_serial.clear(); + self.data.remove_activation_tokens(); } } @@ -256,6 +259,7 @@ pub struct Client { pub symmetric_delete: Cell, pub last_xwayland_serial: Cell, pub surfaces_by_xwayland_serial: CopyHashMap>, + pub activation_tokens: RefCell>, } pub const NUM_CACHED_SERIAL_RANGES: usize = 64; @@ -444,6 +448,12 @@ impl Client { })), } } + + fn remove_activation_tokens(&self) { + for token in &*self.activation_tokens.borrow() { + self.state.activation_tokens.remove(token); + } + } } pub trait WaylandObject: Object { diff --git a/src/compositor.rs b/src/compositor.rs index 917ef34e..8ceeb423 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -199,6 +199,7 @@ fn start_compositor2( workspace_watchers: Default::default(), default_workspace_capture: Cell::new(true), default_gfx_api: Cell::new(GfxApi::OpenGl), + activation_tokens: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -413,6 +414,7 @@ fn create_dummy_output(state: &Rc) { jay_workspaces: Default::default(), capture: Cell::new(false), title_texture: Cell::new(None), + attention_requests: Default::default(), }); dummy_workspace.output_link.set(Some( dummy_output.workspaces.add_last(dummy_workspace.clone()), diff --git a/src/config/handler.rs b/src/config/handler.rs index a285bfc6..e4db824e 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1066,6 +1066,7 @@ impl ConfigProxyHandler { FOCUSED_TITLE_TEXT_COLOR => &colors.focused_title_text, FOCUSED_INACTIVE_TITLE_TEXT_COLOR => &colors.focused_inactive_title_text, BAR_STATUS_TEXT_COLOR => &colors.bar_text, + ATTENTION_REQUESTED_BACKGROUND_COLOR => &colors.attention_requested_background, _ => return Err(CphError::UnknownColor(colorable.0)), }; Ok(colorable) diff --git a/src/globals.rs b/src/globals.rs index 0c78870f..3e2a4156 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -27,6 +27,7 @@ use { wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1Global, wp_tearing_control_manager_v1::WpTearingControlManagerV1Global, wp_viewporter::WpViewporterGlobal, + xdg_activation_v1::XdgActivationV1Global, xdg_wm_base::XdgWmBaseGlobal, zwlr_layer_shell_v1::ZwlrLayerShellV1Global, zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1Global, @@ -162,6 +163,7 @@ impl Globals { add_singleton!(WpSinglePixelBufferManagerV1Global); add_singleton!(WpCursorShapeManagerV1Global); add_singleton!(WpContentTypeManagerV1Global); + add_singleton!(XdgActivationV1Global); } pub fn add_backend_singletons(&self, backend: &Rc) { diff --git a/src/ifs.rs b/src/ifs.rs index 36da165e..bfe9ad91 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -37,6 +37,8 @@ pub mod wp_presentation_feedback; pub mod wp_single_pixel_buffer_manager_v1; pub mod wp_tearing_control_manager_v1; pub mod wp_viewporter; +pub mod xdg_activation_token_v1; +pub mod xdg_activation_v1; pub mod xdg_positioner; pub mod xdg_wm_base; pub mod zwlr_layer_shell_v1; diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index d1a76069..18b4c6c4 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -1059,6 +1059,12 @@ impl WlSurface { pub fn set_content_type(&self, content_type: Option) { self.pending.content_type.set(Some(content_type)); } + + pub fn request_activation(&self) { + if let Some(tl) = self.toplevel.get() { + tl.tl_data().request_attention(tl.tl_as_node()); + } + } } object_base! { diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index fe092165..b13f044b 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -131,7 +131,6 @@ pub struct XwindowData { tree_id!(XwindowId); pub struct Xwindow { pub id: XwindowId, - pub seat_state: NodeSeatState, pub data: Rc, pub x: Rc, pub display_link: RefCell>>>, @@ -214,7 +213,6 @@ impl Xwindow { tld.pos.set(surface.extents.get()); let slf = Rc::new(Self { id: data.state.node_ids.next(), - seat_state: Default::default(), data: data.clone(), display_link: Default::default(), toplevel_data: tld, @@ -298,7 +296,7 @@ impl Node for Xwindow { } fn node_seat_state(&self) -> &NodeSeatState { - &self.seat_state + &self.toplevel_data.seat_state } fn node_visit(self: Rc, visitor: &mut dyn NodeVisitor) { @@ -422,7 +420,7 @@ impl ToplevelNode for Xwindow { fn tl_set_visible(&self, visible: bool) { self.x.surface.set_visible(visible); - self.seat_state.set_visible(self, visible); + self.toplevel_data.set_visible(self, visible); } fn tl_destroy(&self) { diff --git a/src/ifs/xdg_activation_token_v1.rs b/src/ifs/xdg_activation_token_v1.rs new file mode 100644 index 00000000..d9b7e152 --- /dev/null +++ b/src/ifs/xdg_activation_token_v1.rs @@ -0,0 +1,108 @@ +use { + crate::{ + client::{Client, ClientError}, + leaks::Tracker, + object::Object, + utils::{ + activation_token::{activation_token, ActivationToken}, + buffd::{MsgParser, MsgParserError}, + }, + wire::{xdg_activation_token_v1::*, XdgActivationTokenV1Id}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +const MAX_TOKENS_PER_CLIENT: usize = 8; + +pub struct XdgActivationTokenV1 { + pub id: XdgActivationTokenV1Id, + pub client: Rc, + pub tracker: Tracker, + already_used: Cell, +} + +impl XdgActivationTokenV1 { + pub fn new(id: XdgActivationTokenV1Id, client: &Rc) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + already_used: Cell::new(false), + } + } + + fn set_serial(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationTokenV1Error> { + let _req: SetSerial = self.client.parse(self, parser)?; + Ok(()) + } + + fn set_app_id(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationTokenV1Error> { + let _req: SetAppId = self.client.parse(self, parser)?; + Ok(()) + } + + fn set_surface(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationTokenV1Error> { + let req: SetSurface = self.client.parse(self, parser)?; + self.client.lookup(req.surface)?; + Ok(()) + } + + fn commit(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationTokenV1Error> { + let _req: Commit = self.client.parse(self, parser)?; + if self.already_used.replace(true) { + return Err(XdgActivationTokenV1Error::AlreadyUsed); + } + let token = activation_token(); + self.client.state.activation_tokens.set(token, ()); + let mut tokens = self.client.activation_tokens.borrow_mut(); + if tokens.len() >= MAX_TOKENS_PER_CLIENT { + if let Some(oldest) = tokens.pop_front() { + self.client.state.activation_tokens.remove(&oldest); + } + } + tokens.push_back(token); + self.send_done(token); + Ok(()) + } + + fn destroy(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationTokenV1Error> { + let _req: Destroy = self.client.parse(self, parser)?; + self.client.remove_obj(self)?; + Ok(()) + } + + fn send_done(&self, token: ActivationToken) { + let token = token.to_string(); + self.client.event(Done { + self_id: self.id, + token: &token, + }); + } +} + +object_base! { + self = XdgActivationTokenV1; + + SET_SERIAL => set_serial, + SET_APP_ID => set_app_id, + SET_SURFACE => set_surface, + COMMIT => commit, + DESTROY => destroy, +} + +impl Object for XdgActivationTokenV1 {} + +simple_add_obj!(XdgActivationTokenV1); + +#[derive(Debug, Error)] +pub enum XdgActivationTokenV1Error { + #[error(transparent)] + ClientError(Box), + #[error("Parsing failed")] + MsgParserError(#[source] Box), + #[error("The activation token has already been used")] + AlreadyUsed, +} +efrom!(XdgActivationTokenV1Error, ClientError); +efrom!(XdgActivationTokenV1Error, MsgParserError); diff --git a/src/ifs/xdg_activation_v1.rs b/src/ifs/xdg_activation_v1.rs new file mode 100644 index 00000000..068b291c --- /dev/null +++ b/src/ifs/xdg_activation_v1.rs @@ -0,0 +1,120 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName}, + ifs::xdg_activation_token_v1::XdgActivationTokenV1, + leaks::Tracker, + object::Object, + utils::{ + activation_token::ActivationToken, + buffd::{MsgParser, MsgParserError}, + opaque::OpaqueError, + }, + wire::{xdg_activation_v1::*, XdgActivationV1Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct XdgActivationV1Global { + pub name: GlobalName, +} + +impl XdgActivationV1Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: XdgActivationV1Id, + client: &Rc, + version: u32, + ) -> Result<(), XdgActivationV1Error> { + let mgr = Rc::new(XdgActivationV1 { + id, + client: client.clone(), + tracker: Default::default(), + version, + }); + track!(client, mgr); + client.add_client_obj(&mgr)?; + Ok(()) + } +} + +global_base!(XdgActivationV1Global, XdgActivationV1, XdgActivationV1Error); + +simple_add_global!(XdgActivationV1Global); + +impl Global for XdgActivationV1Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 1 + } +} + +pub struct XdgActivationV1 { + pub id: XdgActivationV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: u32, +} + +impl XdgActivationV1 { + fn destroy(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationV1Error> { + let _req: Destroy = self.client.parse(self, parser)?; + self.client.remove_obj(self)?; + Ok(()) + } + + fn get_activation_token(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationV1Error> { + let req: GetActivationToken = self.client.parse(self, parser)?; + let token = Rc::new(XdgActivationTokenV1::new(req.id, &self.client)); + track!(self.client, token); + self.client.add_client_obj(&token)?; + Ok(()) + } + + fn activate(&self, parser: MsgParser<'_, '_>) -> Result<(), XdgActivationV1Error> { + let req: Activate = self.client.parse(self, parser)?; + let token: ActivationToken = req.token.parse()?; + let surface = self.client.lookup(req.surface)?; + if self.client.state.activation_tokens.remove(&token).is_none() { + log::warn!( + "Client requested activation with unknown token {}", + req.token + ); + return Ok(()); + } + surface.request_activation(); + Ok(()) + } +} + +object_base! { + self = XdgActivationV1; + + DESTROY => destroy, + GET_ACTIVATION_TOKEN => get_activation_token, + ACTIVATE => activate, +} + +impl Object for XdgActivationV1 {} + +simple_add_obj!(XdgActivationV1); + +#[derive(Debug, Error)] +pub enum XdgActivationV1Error { + #[error(transparent)] + ClientError(Box), + #[error("Parsing failed")] + MsgParserError(#[source] Box), + #[error("Could not parse the activation token")] + ParseActivationToken(#[from] OpaqueError), +} +efrom!(XdgActivationV1Error, ClientError); +efrom!(XdgActivationV1Error, MsgParserError); diff --git a/src/renderer.rs b/src/renderer.rs index e548c66d..35864769 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -136,6 +136,9 @@ impl Renderer<'_> { }; self.base .fill_boxes2(&rd.captured_inactive_workspaces, &c, x, y); + let c = theme.colors.attention_requested_background.get(); + self.base + .fill_boxes2(&rd.attention_requested_workspaces, &c, x, y); let scale = output.preferred_scale.get(); for title in &rd.titles { let (x, y) = self.base.scale_point(x + title.tex_x, y + title.tex_y); @@ -209,6 +212,8 @@ impl Renderer<'_> { self.base.fill_boxes2(&rd.title_rects, &c, x, y); let c = self.state.theme.colors.focused_title_background.get(); self.base.fill_boxes2(&rd.active_title_rects, &c, x, y); + let c = self.state.theme.colors.attention_requested_background.get(); + self.base.fill_boxes2(&rd.attention_title_rects, &c, x, y); let c = self.state.theme.colors.separator.get(); self.base.fill_boxes2(&rd.underline_rects, &c, x, y); let c = self.state.theme.colors.border.get(); @@ -408,9 +413,12 @@ impl Renderer<'_> { let th = theme.sizes.title_height.get(); let bw = theme.sizes.border_width.get(); let bc = theme.colors.border.get(); - let tc = match floating.active.get() { - true => theme.colors.focused_title_background.get(), - false => theme.colors.unfocused_title_background.get(), + let tc = if floating.active.get() { + theme.colors.focused_title_background.get() + } else if floating.attention_requested.get() { + theme.colors.attention_requested_background.get() + } else { + theme.colors.unfocused_title_background.get() }; let uc = theme.colors.separator.get(); let borders = [ diff --git a/src/state.rs b/src/state.rs index 8677fce2..49cfb508 100644 --- a/src/state.rs +++ b/src/state.rs @@ -42,9 +42,10 @@ use { NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, WorkspaceNode, }, utils::{ - asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap, - errorfmt::ErrorFmt, fdcloser::FdCloser, linkedlist::LinkedList, numcell::NumCell, - queue::AsyncQueue, refcounted::RefCounted, run_toplevel::RunToplevel, + activation_token::ActivationToken, asyncevent::AsyncEvent, clonecell::CloneCell, + copyhashmap::CopyHashMap, errorfmt::ErrorFmt, fdcloser::FdCloser, + linkedlist::LinkedList, numcell::NumCell, queue::AsyncQueue, refcounted::RefCounted, + run_toplevel::RunToplevel, }, video::drm::Drm, wheel::Wheel, @@ -134,6 +135,7 @@ pub struct State { pub workspace_watchers: CopyHashMap<(ClientId, JayWorkspaceWatcherId), Rc>, pub default_workspace_capture: Cell, pub default_gfx_api: Cell, + pub activation_tokens: CopyHashMap, } // impl Drop for State { diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index e3d1a2e9..02494733 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -113,6 +113,7 @@ impl ConnectorHandler { active_workspace: None, underline: Default::default(), inactive_workspaces: Default::default(), + attention_requested_workspaces: Default::default(), captured_inactive_workspaces: Default::default(), titles: Default::default(), status: None, diff --git a/src/theme.rs b/src/theme.rs index 65d63a9e..9803c444 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -157,6 +157,7 @@ colors! { border = (0x3f, 0x47, 0x4a), bar_background = (0x00, 0x00, 0x00), bar_text = (0xff, 0xff, 0xff), + attention_requested_background = (0x23, 0x09, 0x2c), } macro_rules! sizes { diff --git a/src/tree/container.rs b/src/tree/container.rs index aebfdd59..21206d20 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -24,6 +24,7 @@ use { rc_eq::rc_eq, scroller::Scroller, smallmap::{SmallMap, SmallMapMut}, + threshold_counter::ThresholdCounter, }, }, ahash::AHashMap, @@ -83,6 +84,7 @@ pub struct ContainerTitle { pub struct ContainerRenderData { pub title_rects: Vec, pub active_title_rects: Vec, + pub attention_title_rects: Vec, pub last_active_rect: Option, pub border_rects: Vec, pub underline_rects: Vec, @@ -115,6 +117,7 @@ pub struct ContainerNode { pub render_data: RefCell, scroller: Scroller, toplevel_data: ToplevelData, + attention_requests: ThresholdCounter, } impl Debug for ContainerNode { @@ -126,6 +129,7 @@ impl Debug for ContainerNode { pub struct ContainerChild { pub node: Rc, pub active: Cell, + pub attention_requested: Cell, title: RefCell, pub title_tex: SmallMap, pub title_rect: Cell, @@ -172,21 +176,21 @@ impl ContainerNode { ) -> Rc { child.clone().tl_set_workspace(workspace); let children = LinkedList::new(); + let child_node = children.add_last(ContainerChild { + node: child.clone(), + active: Default::default(), + body: Default::default(), + content: Default::default(), + factor: Cell::new(1.0), + title: Default::default(), + title_tex: Default::default(), + title_rect: Default::default(), + focus_history: Default::default(), + attention_requested: Cell::new(false), + }); + let child_node_ref = child_node.clone(); let mut child_nodes = AHashMap::new(); - child_nodes.insert( - child.node_id(), - children.add_last(ContainerChild { - node: child.clone(), - active: Default::default(), - body: Default::default(), - content: Default::default(), - factor: Cell::new(1.0), - title: Default::default(), - title_tex: Default::default(), - title_rect: Default::default(), - focus_history: Default::default(), - }), - ); + child_nodes.insert(child.node_id(), child_node); let slf = Rc::new(Self { id: state.node_ids.next(), parent: CloneCell::new(parent.clone()), @@ -213,9 +217,11 @@ impl ContainerNode { render_data: Default::default(), scroller: Default::default(), toplevel_data: ToplevelData::new(state, Default::default(), None), + attention_requests: Default::default(), }); slf.tl_set_parent(parent); child.tl_set_parent(slf.clone()); + slf.apply_child_flags(&child_node_ref); slf } @@ -293,11 +299,13 @@ impl ContainerNode { title_tex: Default::default(), title_rect: Default::default(), focus_history: Default::default(), + attention_requested: Default::default(), }); let r = link.to_ref(); links.insert(new.node_id(), link); r }; + self.apply_child_flags(&new_ref); new.clone().tl_set_workspace(&self.workspace.get()); new.tl_set_parent(self.clone()); new.tl_set_visible(self.toplevel_data.visible.get()); @@ -644,6 +652,7 @@ impl ContainerNode { } rd.title_rects.clear(); rd.active_title_rects.clear(); + rd.attention_title_rects.clear(); rd.border_rects.clear(); rd.underline_rects.clear(); rd.last_active_rect.take(); @@ -667,6 +676,9 @@ impl ContainerNode { let color = if child.active.get() { rd.active_title_rects.push(rect); theme.colors.focused_title_text.get() + } else if child.attention_requested.get() { + rd.attention_title_rects.push(rect); + theme.colors.unfocused_title_text.get() } else if !have_active && last_active == Some(child.node.node_id()) { rd.last_active_rect = Some(rect); theme.colors.focused_inactive_title_text.get() @@ -760,7 +772,7 @@ impl ContainerNode { return; } let child = { - let children = self.child_nodes.borrow_mut(); + let children = self.child_nodes.borrow(); match child { Some(c) => match children.get(&c.node_id()) { Some(c) => Some(c.to_ref()), @@ -815,7 +827,7 @@ impl ContainerNode { child: &dyn ToplevelNode, direction: Direction, ) { - let child = match self.child_nodes.borrow_mut().get(&child.node_id()) { + let child = match self.child_nodes.borrow().get(&child.node_id()) { Some(c) => c.to_ref(), _ => return, }; @@ -880,7 +892,7 @@ impl ContainerNode { if split == self.split.get() || (split == ContainerSplit::Horizontal && self.mono_child.get().is_some()) { - let cc = match self.child_nodes.borrow_mut().get(&child.node_id()) { + let cc = match self.child_nodes.borrow().get(&child.node_id()) { Some(l) => l.to_ref(), None => return, }; @@ -934,6 +946,33 @@ impl ContainerNode { self.prepend_child(node); } } + + fn apply_child_flags(&self, child: &ContainerChild) { + let data = child.node.tl_data(); + let attention_requested = data.wants_attention.get(); + child.attention_requested.set(attention_requested); + if attention_requested { + self.mod_attention_requests(true); + } + } + + fn discard_child_flags(&self, child: &ContainerChild) { + if child.attention_requested.get() { + self.mod_attention_requests(false); + } + } + + fn mod_attention_requests(&self, set: bool) { + let propagate = self.attention_requests.adj(set); + if set || propagate { + self.toplevel_data.wants_attention.set(set); + } + if propagate { + self.parent + .get() + .cnode_child_attention_request_changed(self, set); + } + } } struct SeatOp { @@ -999,7 +1038,7 @@ impl Node for ContainerNode { } fn node_child_title_changed(self: Rc, child: &dyn Node, title: &str) { - let child = match self.child_nodes.borrow_mut().get(&child.node_id()) { + let child = match self.child_nodes.borrow().get(&child.node_id()) { Some(cn) => cn.to_ref(), _ => return, }; @@ -1083,7 +1122,7 @@ impl Node for ContainerNode { } fn node_child_active_changed(self: Rc, child: &dyn Node, active: bool, depth: u32) { - let node = match self.child_nodes.borrow_mut().get(&child.node_id()) { + let node = match self.child_nodes.borrow().get(&child.node_id()) { Some(l) => l.to_ref(), None => return, }; @@ -1276,6 +1315,7 @@ impl ContainingNode for ContainerNode { None => (false, false), Some(mc) => (true, mc.node.node_id() == old.node_id()), }; + self.discard_child_flags(&node); let link = node.append(ContainerChild { node: new.clone(), active: Cell::new(false), @@ -1286,7 +1326,9 @@ impl ContainingNode for ContainerNode { title_tex: Default::default(), title_rect: Cell::new(node.title_rect.get()), focus_history: Cell::new(None), + attention_requested: Cell::new(false), }); + self.apply_child_flags(&link); if let Some(fh) = node.focus_history.take() { link.focus_history.set(Some(fh.append(link.to_ref()))); } @@ -1314,6 +1356,7 @@ impl ContainingNode for ContainerNode { None => return, }; node.focus_history.set(None); + self.discard_child_flags(&node); if let Some(mono) = self.mono_child.get() { if mono.node.node_id() == child.node_id() { let mut new = self.focus_history.last().map(|n| n.deref().clone()); @@ -1364,6 +1407,19 @@ impl ContainingNode for ContainerNode { fn cnode_accepts_child(&self, _node: &dyn Node) -> bool { true } + + fn cnode_child_attention_request_changed(self: Rc, child: &dyn Node, set: bool) { + let children = self.child_nodes.borrow(); + let child = match children.get(&child.node_id()) { + Some(c) => c, + _ => return, + }; + if child.attention_requested.replace(set) == set { + return; + } + self.mod_attention_requests(set); + self.schedule_compute_render_data(); + } } impl ToplevelNode for ContainerNode { @@ -1429,9 +1485,12 @@ impl ToplevelNode for ContainerNode { } fn tl_set_visible(&self, visible: bool) { - self.toplevel_data.visible.set(visible); - for child in self.children.iter() { - child.node.tl_set_visible(visible); + if let Some(mc) = self.mono_child.get() { + mc.node.tl_set_visible(visible); + } else { + for child in self.children.iter() { + child.node.tl_set_visible(visible); + } } self.toplevel_data.set_visible(self, visible); } diff --git a/src/tree/containing.rs b/src/tree/containing.rs index 1b50b850..5426f631 100644 --- a/src/tree/containing.rs +++ b/src/tree/containing.rs @@ -14,4 +14,5 @@ pub trait ContainingNode: Node { } fn cnode_remove_child2(self: Rc, child: &dyn Node, preserve_focus: bool); fn cnode_accepts_child(&self, node: &dyn Node) -> bool; + fn cnode_child_attention_request_changed(self: Rc, child: &dyn Node, set: bool); } diff --git a/src/tree/float.rs b/src/tree/float.rs index 268cfe83..62f6eeae 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -45,6 +45,7 @@ pub struct FloatNode { pub title: RefCell, pub title_textures: CopyHashMap, seats: RefCell>, + pub attention_requested: Cell, } struct SeatState { @@ -112,7 +113,9 @@ impl FloatNode { title: Default::default(), title_textures: Default::default(), seats: Default::default(), + attention_requested: Cell::new(false), }); + floater.apply_child_flags(); floater .display_link .set(Some(state.root.stacked.add_last(floater.clone()))); @@ -345,6 +348,29 @@ impl FloatNode { self.workspace.set(ws.clone()); self.stacked_set_visible(ws.stacked_visible()); } + + fn apply_child_flags(&self) { + let child = match self.child.get() { + None => return, + Some(c) => c, + }; + let data = child.tl_data(); + let activation_requested = data.wants_attention.get(); + self.attention_requested.set(activation_requested); + if activation_requested { + self.workspace + .get() + .cnode_child_attention_request_changed(self, true); + } + } + + fn discard_child_flags(&self) { + if self.attention_requested.get() { + self.workspace + .get() + .cnode_child_attention_request_changed(self, false); + } + } } impl Debug for FloatNode { @@ -527,13 +553,16 @@ impl ContainingNode for FloatNode { containing_node_impl!(); fn cnode_replace_child(self: Rc, _old: &dyn Node, new: Rc) { + self.discard_child_flags(); self.child.set(Some(new.clone())); + self.apply_child_flags(); new.tl_set_parent(self.clone()); new.clone().tl_set_workspace(&self.workspace.get()); self.schedule_layout(); } fn cnode_remove_child2(self: Rc, _child: &dyn Node, _preserve_focus: bool) { + self.discard_child_flags(); self.child.set(None); self.display_link.set(None); self.workspace_link.set(None); @@ -542,6 +571,14 @@ impl ContainingNode for FloatNode { fn cnode_accepts_child(&self, _node: &dyn Node) -> bool { true } + + fn cnode_child_attention_request_changed(self: Rc, _node: &dyn Node, set: bool) { + if self.attention_requested.replace(set) != set { + self.workspace + .get() + .cnode_child_attention_request_changed(&*self, set); + } + } } impl StackedNode for FloatNode { diff --git a/src/tree/output.rs b/src/tree/output.rs index da518f4b..f66a1bd2 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -144,6 +144,7 @@ impl OutputNode { let mut rd = self.render_data.borrow_mut(); rd.titles.clear(); rd.inactive_workspaces.clear(); + rd.attention_requested_workspaces.clear(); rd.captured_inactive_workspaces.clear(); rd.active_workspace = None; rd.status = None; @@ -219,10 +220,15 @@ impl OutputNode { rect, captured: ws.capture.get(), }); - } else if ws.capture.get() { - rd.captured_inactive_workspaces.push(rect); } else { - rd.inactive_workspaces.push(rect); + if ws.attention_requests.active() { + rd.attention_requested_workspaces.push(rect); + } + if ws.capture.get() { + rd.captured_inactive_workspaces.push(rect); + } else { + rd.inactive_workspaces.push(rect); + } } pos += title_width; } @@ -330,6 +336,7 @@ impl OutputNode { jay_workspaces: Default::default(), capture: self.state.default_workspace_capture.clone(), title_texture: Default::default(), + attention_requests: Default::default(), }); ws.output_link .set(Some(self.workspaces.add_last(ws.clone()))); @@ -493,6 +500,7 @@ pub struct OutputRenderData { pub active_workspace: Option, pub underline: Rect, pub inactive_workspaces: Vec, + pub attention_requested_workspaces: Vec, pub captured_inactive_workspaces: Vec, pub titles: Vec, pub status: Option, diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index e259d550..c5bdcceb 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -179,7 +179,7 @@ impl ToplevelNode for PlaceholderNode { } fn tl_set_visible(&self, visible: bool) { - self.toplevel.visible.set(visible); + self.toplevel.set_visible(self, visible); } fn tl_destroy(&self) { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 8b754be5..44c0fddc 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -5,7 +5,7 @@ use { rect::Rect, state::State, tree::{ContainingNode, Direction, Node, OutputNode, PlaceholderNode, WorkspaceNode}, - utils::{clonecell::CloneCell, numcell::NumCell, smallmap::SmallMap}, + utils::{clonecell::CloneCell, smallmap::SmallMap, threshold_counter::ThresholdCounter}, }, std::{ cell::{Cell, RefCell}, @@ -42,14 +42,14 @@ pub trait ToplevelNode: Node { fn tl_surface_active_changed(&self, active: bool) { let data = self.tl_data(); if active { - if data.active_surfaces.fetch_add(1) == 0 { + if data.active_surfaces.inc() { self.tl_set_active(true); if let Some(parent) = data.parent.get() { parent.node_child_active_changed(self.tl_as_node(), true, 1); } } } else { - if data.active_surfaces.fetch_sub(1) == 1 { + if data.active_surfaces.dec() { self.tl_set_active(false); if let Some(parent) = data.parent.get() { parent.node_child_active_changed(self.tl_as_node(), false, 1); @@ -109,7 +109,7 @@ pub trait ToplevelNode: Node { _ => return, }; let node = self.tl_as_node(); - if data.active.get() || data.active_surfaces.get() > 0 { + if data.active.get() || data.active_surfaces.active() { parent.clone().node_child_active_changed(node, true, 1); } } @@ -161,7 +161,7 @@ pub struct ToplevelData { pub active: Cell, pub client: Option>, pub state: Rc, - pub active_surfaces: NumCell, + pub active_surfaces: ThresholdCounter, pub focus_node: SmallMap, 1>, pub visible: Cell, pub is_floating: Cell, @@ -174,6 +174,8 @@ pub struct ToplevelData { pub parent: CloneCell>>, pub pos: Cell, pub seat_state: NodeSeatState, + pub wants_attention: Cell, + pub requested_attention: Cell, } impl ToplevelData { @@ -195,6 +197,8 @@ impl ToplevelData { parent: Default::default(), pos: Default::default(), seat_state: Default::default(), + wants_attention: Cell::new(false), + requested_attention: Cell::new(false), } } @@ -263,12 +267,10 @@ impl ToplevelData { let mut kb_foci = Default::default(); if ws.visible.get() { if let Some(container) = ws.container.get() { - kb_foci = collect_kb_foci(container.clone()); - container.tl_set_visible(false); + kb_foci = collect_kb_foci(container); } for stacked in ws.stacked.iter() { collect_kb_foci2(stacked.deref().clone().stacked_into_node(), &mut kb_foci); - stacked.stacked_set_visible(false); } } *data = Some(FullscreenedData { @@ -277,7 +279,7 @@ impl ToplevelData { }); drop(data); self.is_fullscreen.set(true); - ws.fullscreen.set(Some(node.clone())); + ws.set_fullscreen_node(&node); node.tl_set_parent(ws.clone()); node.clone().tl_set_workspace(ws); node.clone() @@ -313,11 +315,7 @@ impl ToplevelData { } _ => {} } - fd.workspace.fullscreen.take(); - if node.node_visible() { - fd.workspace.set_visible(true); - fd.workspace.flush_jay_workspaces(); - } + fd.workspace.remove_fullscreen_node(); if fd.placeholder.is_destroyed() { state.map_tiled(node); return; @@ -339,6 +337,31 @@ impl ToplevelData { pub fn set_visible(&self, node: &dyn Node, visible: bool) { self.visible.set(visible); - self.seat_state.set_visible(node, visible) + self.seat_state.set_visible(node, visible); + if !visible { + return; + } + if !self.requested_attention.replace(false) { + return; + } + self.wants_attention.set(false); + if let Some(parent) = self.parent.get() { + parent.cnode_child_attention_request_changed(node, false); + } + self.state.damage(); + } + + pub fn request_attention(&self, node: &dyn Node) { + if self.visible.get() { + return; + } + if self.requested_attention.replace(true) { + return; + } + self.wants_attention.set(true); + if let Some(parent) = self.parent.get() { + parent.cnode_child_attention_request_changed(node, true); + } + self.state.damage(); } } diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 6bb8de77..0e068aa2 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -20,6 +20,7 @@ use { clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::{LinkedList, LinkedNode}, + threshold_counter::ThresholdCounter, }, wire::JayWorkspaceId, }, @@ -45,6 +46,7 @@ pub struct WorkspaceNode { pub jay_workspaces: CopyHashMap<(ClientId, JayWorkspaceId), Rc>, pub capture: Cell, pub title_texture: Cell>, + pub attention_requests: ThresholdCounter, } impl WorkspaceNode { @@ -74,6 +76,10 @@ impl WorkspaceNode { } pub fn set_container(self: &Rc, container: &Rc) { + if let Some(prev) = self.container.get() { + self.discard_child_flags(&*prev); + } + self.apply_child_flags(&**container); let pos = self.position.get(); container.clone().tl_change_extents(&pos); container.clone().tl_set_workspace(self); @@ -103,6 +109,15 @@ impl WorkspaceNode { } } + fn plane_set_visible(&self, visible: bool) { + if let Some(container) = self.container.get() { + container.tl_set_visible(visible); + } + for stacked in self.stacked.iter() { + stacked.stacked_set_visible(visible); + } + } + pub fn set_visible(&self, visible: bool) { for jw in self.jay_workspaces.lock().values() { jw.send_visible(visible); @@ -111,15 +126,52 @@ impl WorkspaceNode { if let Some(fs) = self.fullscreen.get() { fs.tl_set_visible(visible); } else { - if let Some(container) = self.container.get() { - container.tl_set_visible(visible); - } - for stacked in self.stacked.iter() { - stacked.stacked_set_visible(visible); - } + self.plane_set_visible(visible); } self.seat_state.set_visible(self, visible); } + + pub fn set_fullscreen_node(&self, node: &Rc) { + let visible = self.visible.get(); + let mut plane_was_visible = visible; + if let Some(prev) = self.fullscreen.set(Some(node.clone())) { + plane_was_visible = false; + self.discard_child_flags(&*prev); + } + self.apply_child_flags(&**node); + node.tl_set_visible(visible); + if plane_was_visible { + self.plane_set_visible(false); + } + } + + pub fn remove_fullscreen_node(&self) { + if let Some(node) = self.fullscreen.take() { + self.discard_child_flags(&*node); + if self.visible.get() { + self.plane_set_visible(true); + } + } + } + + fn apply_child_flags(&self, child: &dyn ToplevelNode) { + if child.tl_data().wants_attention.get() { + self.mod_attention_requested(true); + } + } + + fn discard_child_flags(&self, child: &dyn ToplevelNode) { + if child.tl_data().wants_attention.get() { + self.mod_attention_requested(false); + } + } + + fn mod_attention_requested(&self, set: bool) { + let crossed_threshold = self.attention_requests.adj(set); + if crossed_threshold { + self.output.get().schedule_update_render_data(); + } + } } impl Node for WorkspaceNode { @@ -224,12 +276,14 @@ impl ContainingNode for WorkspaceNode { fn cnode_remove_child2(self: Rc, child: &dyn Node, _preserve_focus: bool) { if let Some(container) = self.container.get() { if container.node_id() == child.node_id() { + self.discard_child_flags(&*container); self.container.set(None); return; } } if let Some(fs) = self.fullscreen.get() { if fs.tl_as_node().node_id() == child.node_id() { + self.discard_child_flags(&*fs); self.fullscreen.set(None); return; } @@ -240,4 +294,8 @@ impl ContainingNode for WorkspaceNode { fn cnode_accepts_child(&self, node: &dyn Node) -> bool { node.node_is_container() } + + fn cnode_child_attention_request_changed(self: Rc, _node: &dyn Node, set: bool) { + self.mod_attention_requested(set); + } } diff --git a/src/utils.rs b/src/utils.rs index 7cf02f82..6f5435fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +pub mod activation_token; pub mod array; pub mod asyncevent; pub mod bitfield; @@ -18,6 +19,7 @@ pub mod nonblock; pub mod num_cpus; pub mod numcell; pub mod once; +pub mod opaque; pub mod option_ext; pub mod oserror; pub mod page_size; @@ -30,6 +32,7 @@ pub mod scroller; pub mod smallmap; pub mod stack; pub mod syncqueue; +pub mod threshold_counter; pub mod timer; pub mod tri; pub mod trim; diff --git a/src/utils/activation_token.rs b/src/utils/activation_token.rs new file mode 100644 index 00000000..117ab480 --- /dev/null +++ b/src/utils/activation_token.rs @@ -0,0 +1,28 @@ +use { + crate::utils::opaque::{opaque, Opaque, OpaqueError}, + std::{ + fmt::{Display, Formatter}, + str::FromStr, + }, +}; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub struct ActivationToken(Opaque); + +pub fn activation_token() -> ActivationToken { + ActivationToken(opaque()) +} + +impl Display for ActivationToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl FromStr for ActivationToken { + type Err = OpaqueError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } +} diff --git a/src/utils/opaque.rs b/src/utils/opaque.rs new file mode 100644 index 00000000..53fb6e6c --- /dev/null +++ b/src/utils/opaque.rs @@ -0,0 +1,66 @@ +use { + rand::{thread_rng, Rng}, + std::{ + fmt::{Debug, Display, Formatter}, + num::ParseIntError, + str::FromStr, + }, + thiserror::Error, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Opaque { + lo: u64, + hi: u64, +} + +pub fn opaque() -> Opaque { + let mut rng = thread_rng(); + Opaque { + lo: rng.gen(), + hi: rng.gen(), + } +} + +impl Display for Opaque { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:016x}", self.hi)?; + write!(f, "{:016x}", self.lo)?; + Ok(()) + } +} + +impl Debug for Opaque { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +impl FromStr for Opaque { + type Err = OpaqueError; + + fn from_str(s: &str) -> Result { + if s.len() != LEN { + return Err(OpaqueError::InvalidLength); + } + if !s.is_char_boundary(LEN / 2) { + return Err(OpaqueError::NotAscii); + } + let (hi, lo) = s.split_at(LEN / 2); + let hi = u64::from_str_radix(hi, 16).map_err(OpaqueError::Parse)?; + let lo = u64::from_str_radix(lo, 16).map_err(OpaqueError::Parse)?; + Ok(Self { lo, hi }) + } +} + +const LEN: usize = 32; + +#[derive(Debug, Error)] +pub enum OpaqueError { + #[error("The string is not exactly 32 bytes long")] + InvalidLength, + #[error("The string is not ascii")] + NotAscii, + #[error("Could not parse the string as a hex number")] + Parse(ParseIntError), +} diff --git a/src/utils/threshold_counter.rs b/src/utils/threshold_counter.rs new file mode 100644 index 00000000..3fe6f14b --- /dev/null +++ b/src/utils/threshold_counter.rs @@ -0,0 +1,28 @@ +use crate::utils::numcell::NumCell; + +#[derive(Default)] +pub struct ThresholdCounter { + counter: NumCell, +} + +impl ThresholdCounter { + pub fn inc(&self) -> bool { + self.counter.fetch_add(1) == 0 + } + + pub fn dec(&self) -> bool { + self.counter.fetch_sub(1) == 1 + } + + pub fn adj(&self, inc: bool) -> bool { + if inc { + self.inc() + } else { + self.dec() + } + } + + pub fn active(&self) -> bool { + self.counter.get() > 0 + } +} diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index a6e8d637..84539399 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -2234,7 +2234,7 @@ impl Wm { async fn handle_minimize_requested(&self, data: &Rc) -> bool { if let Some(w) = data.window.get() { - if w.toplevel_data.active_surfaces.get() > 0 { + if w.toplevel_data.active_surfaces.active() { self.set_wm_state(data, ICCCM_WM_STATE_NORMAL).await; return false; } diff --git a/wire/xdg_activation_token_v1.txt b/wire/xdg_activation_token_v1.txt new file mode 100644 index 00000000..f9d3aeb9 --- /dev/null +++ b/wire/xdg_activation_token_v1.txt @@ -0,0 +1,28 @@ +# requests + +msg set_serial = 0 { + serial: u32, + seat: id(wl_seat), +} + +msg set_app_id = 1 { + app_id: str, +} + +msg set_surface = 2 { + surface: id(wl_surface), +} + +msg commit = 3 { + +} + +msg destroy = 4 { + +} + +# events + +msg done = 0 { + token: str, +} diff --git a/wire/xdg_activation_v1.txt b/wire/xdg_activation_v1.txt new file mode 100644 index 00000000..34a839ba --- /dev/null +++ b/wire/xdg_activation_v1.txt @@ -0,0 +1,14 @@ +# requests + +msg destroy = 0 { + +} + +msg get_activation_token = 1 { + id: id(xdg_activation_token_v1), +} + +msg activate = 2 { + token: str, + surface: id(wl_surface), +}