use { crate::{ animation::{ RetainedExitLayer, RetainedToplevel, multiphase::{ MultiphaseHierarchyPosition, MultiphaseHierarchyTransition, MultiphaseWindowHierarchy, PhaseAxis, }, }, client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, tlm::{ TL_CHANGED_APP_ID, TL_CHANGED_CONTENT_TY, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, TL_CHANGED_FULLSCREEN, TL_CHANGED_NEW, TL_CHANGED_TITLE, TL_CHANGED_URGENT, TL_CHANGED_VISIBLE, TL_CHANGED_WORKSPACE, TlMatcherChange, }, }, ifs::{ ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, jay_screencast::JayScreencast, jay_toplevel::JayToplevel, wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, wl_surface::{ WlSurface, x_surface::xwindow::XwindowData, xdg_surface::xdg_toplevel::XdgToplevelToplevelData, }, wp_content_type_v1::ContentType, xx_foreign_toplevel_geometry_tracker_v1::XxForeignToplevelGeometryTrackerV1, zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, }, rect::Rect, state::State, tree::{ ContainerNode, ContainerSplit, ContainingNode, Direction, FloatNode, Node, NodeId, NodeLayerLink, OutputNode, PlaceholderNode, WorkspaceNode, }, utils::{ array_to_tuple::ArrayToTuple, clonecell::CloneCell, copyhashmap::CopyHashMap, event_listener::LazyEventSource, hash_map_ext::HashMapExt, numcell::NumCell, rc_eq::rc_eq, threshold_counter::ThresholdCounter, }, wire::{ ExtForeignToplevelHandleV1Id, ExtImageCopyCaptureSessionV1Id, JayScreencastId, JayToplevelId, XxForeignToplevelGeometryTrackerV1Id, ZwlrForeignToplevelHandleV1Id, }, }, jay_config::{window, window::WindowType}, std::{ borrow::Borrow, cell::{Cell, OnceCell, RefCell}, ops::Deref, rc::{Rc, Weak}, }, }; opaque!(ToplevelIdentifier, toplevel_identifier); tree_id!(ToplevelNodeId); pub trait ToplevelNode: ToplevelNodeBase { fn tl_surface_active_changed(&self, active: bool); fn tl_set_fullscreen(self: Rc, fullscreen: bool, ws: Option>); fn tl_title_changed(&self); fn tl_set_parent(&self, parent: Rc); fn tl_extents_changed(&self); fn tl_set_workspace(&self, ws: &Rc); fn tl_workspace_output_changed(&self, prev: &Rc, new: &Rc); 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); fn tl_set_float(&self, float: Option<&Rc>); fn tl_mark_ancestor_fullscreen(&self, fullscreen: bool); fn tl_mark_fullscreen(&self, fullscreen: bool); fn tl_resize(&self, dx1: i32, dy1: i32, dx2: i32, dy2: i32); } impl ToplevelNode for T { fn tl_surface_active_changed(&self, active: bool) { let data = self.tl_data(); data.update_active(self, || { data.active_surfaces.adj(active); }); } fn tl_set_fullscreen(self: Rc, fullscreen: bool, ws: Option>) { let data = self.tl_data(); if fullscreen { if let Some(ws) = ws.or_else(|| data.workspace.get()) { data.set_fullscreen2(&data.state, self.clone(), &ws); } } else { data.unset_fullscreen(&data.state, self.clone()); } } fn tl_title_changed(&self) { let data = self.tl_data(); let title = data.title.borrow_mut(); if let Some(parent) = data.parent.get() { parent.node_child_title_changed(self, &title); } if let Some(data) = data.fullscrceen_data.borrow_mut().deref() { data.placeholder .tl_data() .title .borrow_mut() .clone_from(&title); data.placeholder.tl_title_changed(); } data.property_changed(TL_CHANGED_TITLE); } fn tl_set_parent(&self, parent: Rc) { let data = self.tl_data(); if !data.is_fullscreen.get() { self.tl_mark_ancestor_fullscreen(parent.cnode_self_or_ancestor_fullscreen()); } let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); if parent_was_none { data.mapped_during_iteration.set(data.state.eng.iteration()); data.spawn_in_pending.set(data.kind.is_app_window()); data.property_changed(TL_CHANGED_NEW); } let was_floating = data.parent_is_float.get(); let is_floating = parent.node_is_float(); if was_floating != is_floating { data.property_changed(TL_CHANGED_FLOATING); } data.parent_is_float.set(is_floating); self.tl_set_workspace(&parent.clone().cnode_workspace()); { let float = parent.cnode_get_float(); let prev = data.float.set(float.clone()); let same = match (&prev, &float) { (None, None) => true, (Some(prev), Some(float)) => rc_eq(prev, float), _ => false, }; if !same { self.tl_push_float(float.as_ref()); } } } fn tl_extents_changed(&self) { let data = self.tl_data(); if let Some(parent) = data.parent.get() { let pos = data.pos.get(); parent.node_child_size_changed(self, pos.width(), pos.height()); data.state.tree_changed(); } } fn tl_set_workspace(&self, ws: &Rc) { let data = self.tl_data(); let prev = data.workspace.set(Some(ws.clone())); self.tl_set_workspace_ext(ws); self.tl_data().property_changed(TL_CHANGED_WORKSPACE); let prev_output = match &prev { Some(n) => n.output.get(), _ => ws.state.dummy_output.get().unwrap(), }; let new_output = ws.output.get(); if prev.is_none() || prev_output.id != new_output.id { self.tl_workspace_output_changed(&prev_output, &new_output); } } fn tl_workspace_output_changed(&self, prev: &Rc, new: &Rc) { let data = self.tl_data(); for sc in data.jay_screencasts.lock().values() { sc.update_latch_listener(); } for sc in data.ext_copy_sessions.lock().values() { sc.update_latch_listener(); } if prev.id != new.id { for handle in data.manager_handles.borrow().lock().values() { handle.leave_output(prev); handle.enter_output(new); handle.send_done(); } } } fn tl_change_extents(self: Rc, rect: &Rect) { let data = self.tl_data(); let prev = data.desired_extents.replace(*rect); let target_hierarchy = self.tl_multiphase_hierarchy_position(); let hierarchy = MultiphaseWindowHierarchy::new( data.layout_animation_position.replace(target_hierarchy), target_hierarchy, ); let spawn_in_pending = data.spawn_in_pending.get(); let spawn_in_eligible = spawn_in_pending && !rect.is_empty() && data.visible.get() && !data.is_fullscreen.get() && data.kind.is_app_window() && !self.node_is_container(); let parent_container = data .parent .get() .and_then(|parent| parent.node_into_container()); let parent_is_mono = parent_container .as_ref() .is_some_and(|container| container.mono_child.is_some()); let parent_mono_transition = parent_container .as_ref() .is_some_and(|container| container.mono_transition_animation_pending.get()); let active_mono_boundary = matches!( hierarchy.transition, MultiphaseHierarchyTransition::EnteringMono | MultiphaseHierarchyTransition::ExitingMono ) && parent_mono_transition && (hierarchy.source.mono_active || hierarchy.target.mono_active); if prev != *rect && !prev.is_empty() && !rect.is_empty() && data.visible.get() && !data.parent_is_float.get() && !self.node_is_container() && (!parent_is_mono || active_mono_boundary) { data.state.clone().queue_tiled_animation_with_hierarchy( data.node_id, prev, *rect, hierarchy, ); } if spawn_in_eligible { data.state .clone() .queue_spawn_in_animation(data.node_id, *rect); } if spawn_in_eligible { data.spawn_in_pending.set(false); } if prev.size() != rect.size() { for sc in data.jay_screencasts.lock().values() { sc.schedule_realloc_or_reconfigure(); } for sc in data.ext_copy_sessions.lock().values() { sc.buffer_size_changed(); } } if data.parent_is_float.get() { data.float_width.set(rect.width()); data.float_height.set(rect.height()); } let _ = data; let slf = self.clone(); self.tl_change_extents_impl(rect); let data = slf.tl_data(); for tracker in data.geometry_trackers.lock().values() { tracker.send_geometry_update(rect, &data.state); } } fn tl_set_visible(&self, visible: bool) { self.tl_set_visible_impl(visible); self.tl_data().set_visible(self, visible); } fn tl_destroy(&self) { 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); } fn tl_set_float(&self, float: Option<&Rc>) { self.tl_data().float.set(float.cloned()); self.tl_push_float(float); } fn tl_mark_ancestor_fullscreen(&self, fullscreen: bool) { let old = self .tl_data() .self_or_ancestor_is_fullscreen .replace(fullscreen); if old == fullscreen { return; } self.tl_mark_ancestor_fullscreen_ext(fullscreen); } fn tl_mark_fullscreen(&self, fullscreen: bool) { self.tl_data().is_fullscreen.set(fullscreen); self.tl_mark_ancestor_fullscreen(fullscreen); self.tl_mark_fullscreen_ext(); } fn tl_resize(&self, dx1: i32, dy1: i32, dx2: i32, dy2: i32) { let Some(parent) = self.tl_data().parent.get() else { return; }; let pos = self.node_absolute_position(); let x1 = (dx1 != 0).then_some(pos.x1().saturating_add(dx1)); let y1 = (dy1 != 0).then_some(pos.y1().saturating_add(dy1)); let x2 = (dx2 != 0).then_some(pos.x2().saturating_add(dx2)); let y2 = (dy2 != 0).then_some(pos.y2().saturating_add(dy2)); parent.cnode_resize_child(self, x1, y1, x2, y2); } } pub trait ToplevelNodeBase: Node { fn tl_data(&self) -> &ToplevelData; fn tl_accepts_keyboard_focus(&self) -> bool { true } fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition { let data = self.tl_data(); let Some(parent) = data.parent.get() else { return Default::default(); }; let mut position = MultiphaseHierarchyPosition { parent: Some(parent.node_id()), ..Default::default() }; populate_multiphase_ancestor_splits(&mut position, Some(parent.clone())); if let Some(container) = parent.node_into_container() { position.split_axis = Some(match container.split.get() { ContainerSplit::Horizontal => PhaseAxis::Horizontal, ContainerSplit::Vertical => PhaseAxis::Vertical, }); if let Some(mono) = container.mono_child.get() { position.parent_is_mono = true; position.mono_active = mono.node.node_id() == data.node_id; } for (idx, child) in container.children.iter().enumerate() { if child.node.node_id() == data.node_id { position.sibling_index = Some(idx.min(u16::MAX as usize) as u16); break; } } } position } fn tl_set_active(&self, active: bool) { let _ = active; } fn tl_focus_child(&self) -> Option> { None } fn tl_set_workspace_ext(&self, ws: &Rc) { let _ = ws; } fn tl_change_extents_impl(self: Rc, rect: &Rect); fn tl_close(self: Rc); fn tl_set_visible_impl(&self, visible: bool); fn tl_destroy_impl(&self); fn tl_last_active_child(self: Rc) -> Rc; fn tl_scanout_surface(&self) -> Option> { None } fn tl_animation_snapshot(&self) -> Option> { None } fn tl_restack_popups(&self) { // nothing } fn tl_admits_children(&self) -> bool; fn tl_tile_drag_destination( self: Rc, source: NodeId, split: Option, abs_bounds: Rect, abs_x: i32, abs_y: i32, ) -> Option; fn tl_tile_drag_bounds(&self, split: ContainerSplit, start: bool) -> i32 { let _ = start; default_tile_drag_bounds(self, split) } fn tl_render_bounds(&self) -> Option { self.tl_data() .parent .is_some() .then_some(self.node_absolute_position()) } fn tl_push_float(&self, float: Option<&Rc>) { let _ = float; } fn tl_mark_ancestor_fullscreen_ext(&self, fullscreen: bool) { let _ = fullscreen; } fn tl_mark_fullscreen_ext(&self) { // nothing } } fn populate_multiphase_ancestor_splits( position: &mut MultiphaseHierarchyPosition, mut parent: Option>, ) { let mut depth = 0u16; while let Some(node) = parent { let Some(toplevel) = node.clone().node_into_toplevel() else { break; }; depth = depth.saturating_add(1); if let Some(container) = node.node_into_container() { match container.split.get() { ContainerSplit::Horizontal => { position.nearest_horizontal_split_depth.get_or_insert(depth); } ContainerSplit::Vertical => { position.nearest_vertical_split_depth.get_or_insert(depth); } } } parent = toplevel.tl_data().parent.get(); } position.depth = depth; } pub struct FullscreenedData { pub placeholder: Rc, pub workspace: Rc, } #[derive(Clone)] pub struct ToplevelOpt { toplevel: Weak, identifier: ToplevelIdentifier, } impl ToplevelOpt { pub fn get(&self) -> Option> { let tl = self.toplevel.upgrade()?; if tl.tl_data().identifier.get() == self.identifier { Some(tl) } else { None } } } pub enum ToplevelType { Container, Placeholder(Option), XdgToplevel(Rc), XWindow(Rc), } impl ToplevelType { pub fn to_window_type(&self) -> WindowType { match self { ToplevelType::Container => window::CONTAINER, ToplevelType::Placeholder { .. } => window::PLACEHOLDER, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, ToplevelType::XWindow { .. } => window::X_WINDOW, } } pub fn is_app_window(&self) -> bool { matches!( self, ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_) ) } } pub struct ToplevelData { pub node_id: NodeId, pub kind: ToplevelType, pub self_active: Cell, pub client: Option>, pub state: Rc, pub active_surfaces: ThresholdCounter, pub visible: Cell, pub parent_is_float: Cell, pub float: CloneCell>>, pub float_width: Cell, pub float_height: Cell, pub pinned: Cell, pub is_fullscreen: Cell, pub self_or_ancestor_is_fullscreen: Cell, pub fullscrceen_data: RefCell>, pub workspace: CloneCell>>, pub title: RefCell, pub parent: CloneCell>>, pub mapped_during_iteration: Cell, pub spawn_in_pending: Cell, pub pos: Cell, pub desired_extents: Cell, pub layout_animation_position: Cell, pub seat_state: NodeSeatState, pub wants_attention: Cell, pub requested_attention: Cell, pub app_id: RefCell, pub identifier: Cell, pub handles: CopyHashMap<(ClientId, ExtForeignToplevelHandleV1Id), Rc>, pub geometry_trackers: CopyHashMap< (ClientId, XxForeignToplevelGeometryTrackerV1Id), Rc, >, pub manager_handles: CopyHashMap<(ClientId, ZwlrForeignToplevelHandleV1Id), Rc>, pub render_highlight: NumCell, pub jay_toplevels: CopyHashMap<(ClientId, JayToplevelId), Rc>, pub jay_screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc>, pub ext_copy_sessions: CopyHashMap<(ClientId, ExtImageCopyCaptureSessionV1Id), Rc>, pub slf: Weak, pub destroyed: CopyHashMap>>, pub changed_properties: Cell, pub just_mapped_scheduled: Cell, pub seat_foci: CopyHashMap, pub content_type: Cell>, pub property_changed_source: OnceCell>, pub mapped_source: Rc, pub unmapped_source: Rc, } impl ToplevelData { pub fn new( state: &Rc, title: String, client: Option>, kind: ToplevelType, node_id: impl Into, slf: &Weak, ) -> Self { let node_id = node_id.into(); let id = toplevel_identifier(); state.toplevels.set(id, slf.clone()); Self { node_id, kind, self_active: Cell::new(false), client, state: state.clone(), active_surfaces: Default::default(), visible: Cell::new(false), parent_is_float: Default::default(), float: Default::default(), float_width: Default::default(), float_height: Default::default(), pinned: Cell::new(false), is_fullscreen: Default::default(), self_or_ancestor_is_fullscreen: Default::default(), fullscrceen_data: Default::default(), workspace: Default::default(), title: RefCell::new(title), parent: Default::default(), mapped_during_iteration: Cell::new(0), spawn_in_pending: Cell::new(false), pos: Default::default(), desired_extents: Default::default(), layout_animation_position: Default::default(), seat_state: Default::default(), wants_attention: Cell::new(false), requested_attention: Cell::new(false), app_id: Default::default(), identifier: Cell::new(id), handles: Default::default(), geometry_trackers: Default::default(), manager_handles: Default::default(), render_highlight: Default::default(), jay_toplevels: Default::default(), jay_screencasts: Default::default(), ext_copy_sessions: Default::default(), slf: slf.clone(), destroyed: Default::default(), changed_properties: Default::default(), just_mapped_scheduled: Cell::new(false), seat_foci: Default::default(), content_type: Default::default(), property_changed_source: Default::default(), mapped_source: state.lazy_event_sources.create_source(), unmapped_source: state.lazy_event_sources.create_source(), } } pub fn active(&self) -> bool { self.active_surfaces.active() || self.self_active.get() } fn update_active(&self, tl: &T, f: F) { let active_old = self.active(); f(); let active_new = self.active(); if active_old != active_new { tl.tl_set_active(active_new); if let Some(parent) = self.parent.get() { parent.node_child_active_changed(tl, active_new, 1); } for handle in self.manager_handles.borrow().lock().values() { handle.send_state(active_new, self.is_fullscreen.get()); handle.send_done(); } } } pub fn update_self_active(&self, node: &T, active: bool) { self.update_active(node, || self.self_active.set(active)); } pub fn float_size(&self, ws: &WorkspaceNode) -> (i32, i32) { let output = ws.output.get().global.pos.get(); let mut width = self.float_width.get(); let mut height = self.float_height.get(); if width == 0 { width = output.width() / 2; } if height == 0 { height = output.height() / 2; } (width, height) } fn trigger_property_source(&self) { if let Some(source) = self.property_changed_source.get() { source.trigger(); } } pub fn property_changed(&self, change: TlMatcherChange) { self.trigger_property_source(); let mgr = &self.state.tl_matcher_manager; let props = self.changed_properties.get(); if props.is_none() && mgr.has_no_interest(self, change) { return; } self.changed_properties.set(props | change); if props.is_none() && change.is_some() && let Some(node) = self.slf.upgrade() { mgr.changed(node); } } pub fn destroy_node(&self, node: &dyn Node) { for jay_tl in self.jay_toplevels.lock().drain_values() { jay_tl.destroy(); } for screencast in self.jay_screencasts.lock().drain_values() { screencast.do_destroy(); } for screencast in self.ext_copy_sessions.lock().drain_values() { screencast.stop(); } { let id = toplevel_identifier(); let prev = self.identifier.replace(id); self.state.remove_toplevel_id(prev); self.state.toplevels.set(id, self.slf.clone()); } { let mut handles = self.handles.lock(); for handle in handles.drain_values() { handle.send_closed(); } } { let mut manager_handles = self.manager_handles.lock(); for handle in manager_handles.drain_values() { handle.send_closed(); } } { let mut trackers = self.geometry_trackers.lock(); for tracker in trackers.drain_values() { tracker.send_finished(); } } self.detach_node(node); self.property_changed(TL_CHANGED_DESTROYED); } pub fn detach_node(&self, node: &dyn Node) { if let Some(fd) = self.fullscrceen_data.borrow_mut().take() { fd.placeholder.tl_destroy(); } if let Some(parent) = self.parent.take() { parent.cnode_remove_child(node); } self.float.take(); self.workspace.take(); self.seat_state.destroy_node(node); } pub fn broadcast(&self, toplevel: Rc) { let id = self.identifier.get().to_string(); let title = self.title.borrow(); let app_id = self.app_id.borrow(); let activated = self.active(); let fullscreen = self.is_fullscreen.get(); let class; let manager_app_id = match &self.kind { ToplevelType::XWindow(w) => { class = w.info.class.borrow(); class.as_deref().unwrap_or_default() } _ => &app_id, }; for list in self.state.toplevel_lists.lock().values() { self.send_once(&toplevel, list, &id, &title, &app_id); } for manager in self.state.toplevel_managers.lock().values() { self.manager_send_once( &toplevel, manager, &title, manager_app_id, activated, fullscreen, ); } } pub fn send(&self, toplevel: Rc, list: &ExtForeignToplevelListV1) { let id = self.identifier.get().to_string(); let title = self.title.borrow(); let app_id = self.app_id.borrow(); self.send_once(&toplevel, list, &id, &title, &app_id); } fn send_once( &self, toplevel: &Rc, list: &ExtForeignToplevelListV1, id: &str, title: &str, app_id: &str, ) { let opt = ToplevelOpt { toplevel: Rc::downgrade(toplevel), identifier: self.identifier.get(), }; let handle = match list.publish_toplevel(opt) { None => return, Some(handle) => handle, }; handle.send_identifier(id); handle.send_title(title); handle.send_app_id(app_id); handle.send_done(); self.handles .set((handle.client.id, handle.id), handle.clone()); } pub fn manager_send( &self, toplevel: Rc, manager: &ZwlrForeignToplevelManagerV1, ) { let title = self.title.borrow(); let activated = self.active(); let fullscreen = self.is_fullscreen.get(); let app_id; let class; let manager_app_id = match &self.kind { ToplevelType::XWindow(w) => { class = w.info.class.borrow(); class.as_deref().unwrap_or_default() } _ => { app_id = self.app_id.borrow(); &app_id } }; self.manager_send_once( &toplevel, manager, &title, manager_app_id, activated, fullscreen, ); } fn manager_send_once( &self, toplevel: &Rc, manager: &ZwlrForeignToplevelManagerV1, title: &str, app_id: &str, activated: bool, fullscreen: bool, ) { let opt = ToplevelOpt { toplevel: Rc::downgrade(toplevel), identifier: self.identifier.get(), }; let handle = match manager.publish_toplevel(opt) { None => return, Some(handle) => handle, }; if !app_id.is_empty() { handle.send_app_id(app_id); } if !title.is_empty() { handle.send_title(title); } handle.enter_output(&self.output()); handle.send_state(activated, fullscreen); handle.send_done(); self.manager_handles .set((handle.client.id, handle.id), handle.clone()); } pub fn set_title(&self, title: &str) { *self.title.borrow_mut() = title.to_string(); for handle in self.handles.lock().values() { handle.send_title(title); handle.send_done(); } for handle in self.manager_handles.lock().values() { handle.send_title(title); handle.send_done(); } } pub fn set_app_id(&self, app_id: &str) { let dst = &mut *self.app_id.borrow_mut(); if *dst == app_id { return; } *dst = app_id.to_string(); for handle in self.handles.lock().values() { handle.send_app_id(app_id); handle.send_done(); } for handle in self.manager_handles.lock().values() { handle.send_app_id(app_id); handle.send_done(); } self.property_changed(TL_CHANGED_APP_ID) } pub fn set_fullscreen( &self, state: &Rc, node: Rc, output: &Rc, ) { self.set_fullscreen2(state, node, &output.ensure_workspace()); } pub fn set_fullscreen2( &self, state: &Rc, node: Rc, ws: &Rc, ) { if ws.fullscreen.is_some() { log::info!( "Cannot fullscreen a node on a workspace that already has a fullscreen node attached" ); return; } if node.node_is_placeholder() { log::info!("Cannot fullscreen a placeholder node"); return; } let mut data = self.fullscrceen_data.borrow_mut(); if data.is_some() { log::info!("Cannot fullscreen a node that is already fullscreen"); return; } let parent = match node.tl_data().parent.get() { None => { log::warn!("Cannot fullscreen a node without a parent"); return; } Some(p) => p, }; if parent.node_is_workspace() { log::warn!("Cannot fullscreen root container in a workspace"); return; } let placeholder = Rc::new_cyclic(|weak| PlaceholderNode::new_for(state, node.clone(), weak)); parent.cnode_replace_child(&*node, placeholder.clone()); let mut kb_foci = Default::default(); if ws.visible.get() { if let Some(container) = ws.container.get() { kb_foci = collect_kb_foci(container); } for stacked in ws.stacked.iter() { collect_kb_foci2(stacked.deref().clone(), &mut kb_foci); } } *data = Some(FullscreenedData { placeholder, workspace: ws.clone(), }); drop(data); node.tl_mark_fullscreen(true); self.property_changed(TL_CHANGED_FULLSCREEN); node.tl_set_parent(ws.clone()); ws.set_fullscreen_node(&node); node.clone() .tl_change_extents(&ws.output.get().global.pos.get()); for seat in kb_foci { node.clone().node_do_focus(&seat, Direction::Unspecified); } for handle in self.manager_handles.lock().values() { handle.send_state(self.active(), true); handle.send_done(); } } pub fn unset_fullscreen(&self, state: &Rc, node: Rc) { if !self.is_fullscreen.get() { log::warn!("Cannot unset fullscreen on a node that is not fullscreen"); return; } let fd = match self.fullscrceen_data.borrow_mut().take() { Some(fd) => fd, _ => { log::error!("is_fullscreen = true but data is None"); return; } }; node.tl_mark_fullscreen(false); self.property_changed(TL_CHANGED_FULLSCREEN); match fd.workspace.fullscreen.get() { None => { log::error!( "Node is supposed to be fullscreened on a workspace but workspace has not fullscreen node." ); return; } Some(f) if f.node_id() != node.node_id() => { log::error!( "Node is supposed to be fullscreened on a workspace but the workspace has a different node attached." ); return; } _ => {} } fd.workspace.remove_fullscreen_node(); if fd.placeholder.is_destroyed() { state.map_tiled_without_autotile(node); return; } let parent = fd.placeholder.tl_data().parent.take().unwrap(); parent.clone().cnode_make_visible(&*fd.placeholder); parent.cnode_replace_child(fd.placeholder.deref(), node.clone()); if node.node_visible() { let kb_foci = collect_kb_foci(fd.placeholder.clone()); for seat in kb_foci { node.clone().node_do_focus(&seat, Direction::Unspecified); } } fd.placeholder.tl_destroy(); for handle in self.manager_handles.lock().values() { handle.send_state(self.active(), false); handle.send_done(); } } pub fn set_visible(&self, node: &dyn Node, visible: bool) { if self.visible.replace(visible) != visible { self.property_changed(TL_CHANGED_VISIBLE); } self.seat_state.set_visible(node, visible); for sc in self.jay_screencasts.lock().values() { sc.update_latch_listener(); } for sc in self.ext_copy_sessions.lock().values() { sc.update_latch_listener(); } if !visible { return; } if !self.requested_attention.replace(false) { return; } self.set_wants_attention(false); if let Some(parent) = self.parent.get() { parent.cnode_child_attention_request_changed(node, false); } } pub fn request_attention(&self, node: &dyn Node) { if self.visible.get() { return; } if self.requested_attention.replace(true) { return; } self.set_wants_attention(true); if let Some(parent) = self.parent.get() { parent.cnode_child_attention_request_changed(node, true); } } pub fn set_wants_attention(&self, value: bool) { if self.wants_attention.replace(value) != value { self.property_changed(TL_CHANGED_URGENT); } } pub fn output(&self) -> Rc { match self.output_opt() { None => self.state.dummy_output.get().unwrap(), Some(o) => o, } } pub fn output_opt(&self) -> Option> { self.workspace.get().map(|ws| ws.output.get()) } pub fn desired_pixel_size(&self) -> (i32, i32) { let (dw, dh) = self.desired_extents.get().size(); if let Some(ws) = self.workspace.get() { let scale = ws.output.get().global.persistent.scale.get(); return scale.pixel_size([dw, dh]).to_tuple(); }; (0, 0) } pub fn just_mapped(&self) -> bool { self.mapped_during_iteration.get() == self.state.eng.iteration() } pub fn queue_spawn_out(&self, node: &dyn ToplevelNode, retained: Option>) { if !self.kind.is_app_window() || !self.visible.get() || self.is_fullscreen.get() || node.node_is_container() { return; } let Some(retained) = retained else { return; }; let bw = self.state.theme.sizes.border_width.get().max(0); let now = self.state.now_nsec(); let (outer, frame_inset, layer) = if self.parent_is_float.get() { let Some(float) = self.float.get() else { return; }; ( self.state .animations .visual_rect(float.node_id(), float.position.get(), now), bw, RetainedExitLayer::Floating, ) } else { let body = self.state .animations .visual_rect(self.node_id, node.node_absolute_position(), now); if body.is_empty() { return; } if self.state.theme.sizes.gap.get() != 0 { ( Rect::new_sized_saturating( body.x1() - bw, body.y1() - bw, body.width() + 2 * bw, body.height() + 2 * bw, ), bw, RetainedExitLayer::Tiled, ) } else { (body, 0, RetainedExitLayer::Tiled) } }; self.state.clone().queue_spawn_out_animation( outer, frame_inset, retained, self.active(), layer, ); } pub fn set_content_type(&self, content_type: Option) { if self.content_type.replace(content_type) != content_type { self.property_changed(TL_CHANGED_CONTENT_TY); } } pub fn make_visible(&self, slf: &dyn Node) { if self.visible.get() { return; } if let Some(parent) = self.parent.get() { parent.cnode_make_visible(slf); } } pub fn node_layer(&self) -> NodeLayerLink { if self.self_or_ancestor_is_fullscreen.get() { return NodeLayerLink::Fullscreen; } if let Some(float) = self.float.get() { return float.node_layer(); } NodeLayerLink::Tiled } pub fn is_root_container(&self) -> bool { if not_matches!(self.kind, ToplevelType::Container) { return false; } let Some(parent) = self.parent.get() else { return false; }; parent.node_is_workspace() } } impl Drop for ToplevelData { fn drop(&mut self) { self.state.remove_toplevel_id(self.identifier.get()); } } pub struct TileDragDestination { pub highlight: Rect, pub ty: TddType, } pub enum TddType { Replace(Rc), Split { node: Rc, split: ContainerSplit, before: bool, }, Insert { container: Rc, neighbor: Rc, before: bool, }, NewWorkspace { output: Rc, }, NewContainer { workspace: Rc, }, MoveToWorkspace { workspace: Rc, }, MoveToNewWorkspace { output: Rc, }, } pub fn default_tile_drag_bounds(t: &T, split: ContainerSplit) -> i32 { const FACTOR: i32 = 5; match split { ContainerSplit::Horizontal => t.node_absolute_position().width() / FACTOR, ContainerSplit::Vertical => t.node_absolute_position().height() / FACTOR, } } pub fn toplevel_parent_container(tl: &dyn ToplevelNode) -> Option> { if let Some(parent) = tl.tl_data().parent.get() && let Some(container) = parent.node_into_container() { return Some(container); } None } pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: ContainerSplit) { if tl.tl_data().is_fullscreen.get() { return; } let ws = match tl.tl_data().workspace.get() { Some(ws) => ws, _ => return, }; let pn = match tl.tl_data().parent.get() { Some(pn) => pn, _ => return, }; if let Some(pn) = pn.node_into_containing_node() { let cn = ContainerNode::new(state, &ws, tl.clone(), axis); pn.cnode_replace_child(&*tl, cn); } } fn float_outer_for_body(state: &State, body: Rect) -> Rect { let bw = state.theme.sizes.border_width.get(); Rect::new_sized_saturating( body.x1() - bw, body.y1() - bw, body.width() + 2 * bw, body.height() + 2 * bw, ) } fn float_body_for_outer(state: &State, outer: Rect) -> Rect { let bw = state.theme.sizes.border_width.get(); Rect::new_sized_saturating( outer.x1() + bw, outer.y1() + bw, outer.width() - 2 * bw, outer.height() - 2 * bw, ) } pub fn toplevel_set_floating(state: &Rc, tl: Rc, floating: bool) { let data = tl.tl_data(); if data.is_fullscreen.get() { return; } if data.parent_is_float.get() == floating { return; } let parent = match data.parent.get() { Some(p) => p, _ => return, }; if !floating { parent.cnode_remove_child2(&*tl, true); state.map_tiled_without_autotile(tl); } else if let Some(ws) = data.workspace.get() { let node_id = data.node_id; let old_body = state .animations .visual_rect(node_id, tl.node_absolute_position(), state.now_nsec()); let old_outer = float_outer_for_body(state, old_body); parent.cnode_remove_child2(&*tl, true); let (width, height) = data.float_size(&ws); let floater = state.map_floating(tl, width, height, &ws, None); let new_outer = floater.position.get(); let new_body = float_body_for_outer(state, new_outer); state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer); state.queue_linear_layout_animation(node_id, old_body, new_body); } } pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: &Rc) { let old_ws = match tl.tl_data().workspace.get() { Some(ws) => ws, _ => return, }; if old_ws.id == ws.id { return; } let data = tl.tl_data(); let fullscreen = data.is_fullscreen.get(); if fullscreen { if let Some(old) = ws.fullscreen.get() { old.tl_set_fullscreen(false, None); } if ws.fullscreen.is_some() { return; } tl.clone().tl_set_fullscreen(false, None); if data.is_fullscreen.get() { return; } } let cn = match tl.tl_data().parent.get() { Some(cn) => cn, _ => return, }; let kb_foci = collect_kb_foci(tl.clone()); cn.cnode_remove_child2(&*tl, true); if !ws.visible.get() { for focus in kb_foci { old_ws.clone().node_do_focus(&focus, Direction::Unspecified); } } if tl.tl_data().parent_is_float.get() { let (width, height) = tl.tl_data().float_size(ws); state.map_floating(tl.clone(), width, height, ws, None); } else { state.map_tiled_on(tl.clone(), ws); } if fullscreen { tl.tl_set_fullscreen(true, Some(ws.clone())); } }