1use crate::{
3 core::{
4 client::Client,
5 data_types::Region,
6 hooks::HookName,
7 layout::LayoutConf,
8 manager::{event::EventAction, util::pad_region},
9 ring::Selector,
10 workspace::ArrangeActions,
11 xconnection::{
12 Atom, ClientMessageKind, Prop, XClientConfig, XClientHandler, XClientProperties,
13 XEventHandler, XState, Xid,
14 },
15 },
16 draw::Color,
17 Result,
18};
19use std::collections::HashMap;
20use tracing::{trace, warn};
21
22#[derive(Debug)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24pub(super) struct Clients {
25 inner: HashMap<Xid, Client>,
26 focused_client_id: Option<Xid>,
27 focused_border: Color,
28 unfocused_border: Color,
29}
30
31impl Clients {
32 pub fn new(focused_border: impl Into<Color>, unfocused_border: impl Into<Color>) -> Self {
33 Self {
34 inner: HashMap::new(),
35 focused_client_id: None,
36 focused_border: focused_border.into(),
37 unfocused_border: unfocused_border.into(),
38 }
39 }
40
41 pub fn is_known(&self, id: Xid) -> bool {
42 self.inner.contains_key(&id)
43 }
44
45 pub fn focused_client_id(&self) -> Option<Xid> {
46 self.focused_client_id
47 }
48
49 pub fn focused_client(&self) -> Option<&Client> {
50 self.focused_client_id.and_then(|id| self.inner.get(&id))
51 }
52
53 pub fn focused_client_mut(&mut self) -> Option<&mut Client> {
54 self.focused_client_id
55 .and_then(move |id| self.inner.get_mut(&id))
56 }
57
58 pub fn client(&self, selector: &Selector<'_, Client>) -> Option<&Client> {
59 match selector {
60 Selector::Focused | Selector::Any => self.focused_client(),
61 Selector::WinId(id) => self.inner.get(&id),
62 Selector::Condition(f) => self.inner.iter().find(|(_, v)| f(v)).map(|(_, v)| v),
63 Selector::Index(i) => self.inner.iter().nth(*i).map(|(_, c)| c),
64 }
65 }
66
67 pub fn client_mut(&mut self, selector: &Selector<'_, Client>) -> Option<&mut Client> {
68 match selector {
69 Selector::Focused | Selector::Any => self.focused_client_mut(),
70 Selector::WinId(id) => self.inner.get_mut(&id),
71 Selector::Condition(f) => self.inner.iter_mut().find(|(_, v)| f(v)).map(|(_, v)| v),
72 Selector::Index(i) => self.inner.iter_mut().nth(*i).map(|(_, c)| c),
73 }
74 }
75
76 pub fn matching_clients(&self, selector: &Selector<'_, Client>) -> Vec<&Client> {
77 let mut clients: Vec<&Client> = match selector {
78 Selector::Any => self.inner.values().collect(),
79 Selector::Focused => self.focused_client().into_iter().collect(),
80 Selector::WinId(id) => self.inner.get(&id).into_iter().collect(),
81 Selector::Condition(f) => self.inner.values().filter(|v| f(v)).collect(),
82 _ => self.client(selector).into_iter().collect(),
83 };
84
85 clients.sort_unstable_by_key(|c| c.id());
86 clients
87 }
88
89 pub fn matching_clients_mut(&mut self, selector: &Selector<'_, Client>) -> Vec<&mut Client> {
90 let mut clients: Vec<&mut Client> = match selector {
91 Selector::Any => self.inner.values_mut().collect(),
92 Selector::Focused => self.focused_client_mut().into_iter().collect(),
93 Selector::WinId(id) => self.inner.get_mut(&id).into_iter().collect(),
94 Selector::Condition(f) => self.inner.values_mut().filter(|v| f(v)).collect(),
95 _ => self.client_mut(selector).into_iter().collect(),
96 };
97
98 clients.sort_unstable_by_key(|c| c.id());
99 clients
100 }
101
102 pub fn set_focused<X>(&mut self, id: Xid, conn: &X) -> Option<Xid>
103 where
104 X: XClientConfig,
105 {
106 let prev = self.focused_client_id;
107 self.focused_client_id = Some(id);
108
109 if let Some(prev_id) = prev {
110 if id != prev_id {
111 self.client_lost_focus(prev_id, conn);
112 }
113 }
114
115 prev
116 }
117
118 #[allow(dead_code)]
119 pub fn clear_focused(&mut self) {
120 self.focused_client_id = None
121 }
122
123 pub fn insert(&mut self, id: Xid, c: Client) -> Option<Client> {
124 self.inner.insert(id, c)
125 }
126
127 pub fn remove(&mut self, id: Xid) -> Option<Client> {
128 if self.focused_client_id == Some(id) {
129 self.focused_client_id = None;
130 }
131
132 self.inner.remove(&id)
133 }
134
135 pub fn get(&self, id: Xid) -> Option<&Client> {
136 self.inner.get(&id)
137 }
138
139 pub fn get_mut(&mut self, id: Xid) -> Option<&mut Client> {
140 self.inner.get_mut(&id)
141 }
142
143 pub fn set_client_workspace(&mut self, id: Xid, wix: usize) {
144 self.inner.entry(id).and_modify(|c| c.set_workspace(wix));
145 }
146
147 pub fn map_if_needed<X>(&mut self, id: Xid, conn: &X) -> Result<()>
148 where
149 X: XClientHandler,
150 {
151 Ok(conn.map_client_if_needed(self.inner.get_mut(&id))?)
152 }
153
154 pub fn unmap_if_needed<X>(&mut self, id: Xid, conn: &X) -> Result<()>
155 where
156 X: XClientHandler,
157 {
158 Ok(conn.unmap_client_if_needed(self.inner.get_mut(&id))?)
159 }
160
161 pub fn workspace_index_for_client(&self, id: Xid) -> Option<usize> {
166 self.inner.get(&id).map(|c| c.workspace())
167 }
168
169 pub fn clients_for_workspace(&self, wix: usize) -> Vec<&Client> {
170 self.matching_clients(&Selector::Condition(&|c: &Client| c.workspace == wix))
171 }
172
173 pub fn clients_for_ids(&self, ids: &[Xid]) -> Vec<&Client> {
174 ids.iter().map(|i| &self.inner[i]).collect()
175 }
176
177 pub fn all_known_ids(&self) -> Vec<Xid> {
178 self.inner.keys().copied().collect()
179 }
180
181 pub fn modify(&mut self, id: Xid, f: impl Fn(&mut Client)) {
182 self.inner.entry(id).and_modify(f);
183 }
184
185 pub fn set_x_focus<X>(&self, id: Xid, accepts_focus: bool, conn: &X) -> Result<()>
188 where
189 X: XState + XEventHandler + XClientConfig + XClientHandler + XClientProperties,
190 {
191 trace!(id, accepts_focus, "setting focus");
192 if accepts_focus {
193 if let Err(e) = conn.focus_client(id) {
194 warn!("unable to focus client {}: {}", id, e);
195 }
196 conn.change_prop(
197 conn.root(),
198 Atom::NetActiveWindow.as_ref(),
199 Prop::Window(vec![id]),
200 )?;
201 let fb = self.focused_border;
202 if let Err(e) = conn.set_client_border_color(id, fb) {
203 warn!("unable to set client border color for {}: {}", id, e);
204 }
205 } else {
206 let msg = ClientMessageKind::TakeFocus(id).as_message(conn)?;
207 conn.send_client_event(msg)?;
208 }
209
210 Ok(())
212 }
213
214 pub fn focus_in<X>(&self, id: Xid, conn: &X) -> Result<()>
215 where
216 X: XState + XEventHandler + XClientConfig + XClientHandler + XClientProperties,
217 {
218 let accepts_focus = match self.inner.get(&id) {
219 Some(client) => client.accepts_focus,
220 None => conn.client_accepts_focus(id),
221 };
222
223 self.set_x_focus(id, accepts_focus, conn)
224 }
225
226 #[tracing::instrument(level = "trace", skip(self, conn))]
228 pub fn client_lost_focus<X>(&mut self, id: Xid, conn: &X)
229 where
230 X: XClientConfig,
231 {
232 if self.focused_client_id == Some(id) {
233 self.focused_client_id = None;
234 }
235
236 if self.inner.contains_key(&id) {
237 let ub = self.unfocused_border;
238 conn.set_client_border_color(id, ub).unwrap_or(());
241 }
242 }
243
244 pub fn client_name_changed<X>(
246 &mut self,
247 id: Xid,
248 is_root: bool,
249 conn: &X,
250 ) -> Result<EventAction>
251 where
252 X: XClientProperties,
253 {
254 let name = conn.client_name(id)?;
255 if !is_root {
256 if let Some(c) = self.inner.get_mut(&id) {
257 c.set_name(&name)
258 }
259 }
260
261 Ok(EventAction::RunHook(HookName::ClientNameUpdated(
262 id, name, is_root,
263 )))
264 }
265
266 pub fn apply_arrange_actions<X>(
267 &mut self,
268 actions: ArrangeActions,
269 lc: &LayoutConf,
270 border_px: u32,
271 gap_px: u32,
272 conn: &X,
273 ) -> Result<()>
274 where
275 X: XClientHandler + XClientConfig,
276 {
277 for (id, region) in actions.actions {
279 trace!(id, ?region, "positioning client");
280 if let Some(region) = region {
281 let reg = pad_region(®ion, lc.gapless, gap_px, border_px);
282 conn.position_client(id, reg, border_px, false)?;
283 self.map_if_needed(id, conn)?;
284 } else {
285 self.unmap_if_needed(id, conn)?;
286 }
287 }
288
289 for id in actions.floating {
290 debug!(id, "mapping floating client above tiled");
291 conn.raise_client(id)?;
292 }
293
294 Ok(())
295 }
296
297 pub fn toggle_fullscreen<X>(
298 &mut self,
299 id: Xid,
300 wix: usize,
301 workspace_clients: &[Xid],
302 screen_size: Region,
303 conn: &X,
304 ) -> Result<Vec<EventAction>>
305 where
306 X: XClientHandler + XClientProperties + XClientConfig,
307 {
308 let client_currently_fullscreen = match self.get(id) {
309 Some(c) => c.fullscreen,
310 None => {
311 warn!(id, "attempt to make unknown client fullscreen");
312 return Ok(vec![]);
313 }
314 };
315
316 conn.toggle_client_fullscreen(id, client_currently_fullscreen)?;
317
318 for &i in workspace_clients.iter() {
319 if client_currently_fullscreen {
320 if i == id {
321 self.inner.entry(id).and_modify(|c| c.fullscreen = false);
322 } else {
323 self.map_if_needed(i, conn)?;
324 }
325 } else if i == id {
327 conn.position_client(id, screen_size, 0, false)?;
328 let is_known = self.is_known(id);
329 if is_known {
330 self.map_if_needed(id, conn)?;
331 self.modify(id, |c| c.fullscreen = true);
332 }
333 } else {
334 self.unmap_if_needed(i, conn)?;
335 }
336 }
337
338 Ok(if client_currently_fullscreen {
339 vec![EventAction::LayoutWorkspace(wix)]
340 } else {
341 vec![]
342 })
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::core::xconnection::{self, *};
350 use std::cell::Cell;
351
352 #[test]
353 fn client_lost_focus_on_focused_clears_focused_client_id() {
354 let conn = MockXConn::new(vec![], vec![], vec![]);
355 let mut clients = Clients::new(0xffffff, 0x000000);
356
357 clients.focused_client_id = Some(42);
358 clients.client_lost_focus(42, &conn);
359 assert!(clients.focused_client_id.is_none());
360 }
361
362 struct RecordingXConn {
363 positions: Cell<Vec<(Xid, Region)>>,
364 maps: Cell<Vec<Xid>>,
365 unmaps: Cell<Vec<Xid>>,
366 }
367
368 impl RecordingXConn {
369 fn init() -> Self {
370 Self {
371 positions: Cell::new(Vec::new()),
372 maps: Cell::new(Vec::new()),
373 unmaps: Cell::new(Vec::new()),
374 }
375 }
376 }
377
378 impl StubXClientProperties for RecordingXConn {}
379
380 impl StubXClientHandler for RecordingXConn {
381 fn mock_map_client(&self, id: Xid) -> xconnection::Result<()> {
382 let mut v = self.maps.take();
383 v.push(id);
384 self.maps.set(v);
385 Ok(())
386 }
387
388 fn mock_unmap_client(&self, id: Xid) -> xconnection::Result<()> {
389 let mut v = self.unmaps.take();
390 v.push(id);
391 self.unmaps.set(v);
392 Ok(())
393 }
394 }
395
396 impl StubXClientConfig for RecordingXConn {
397 fn mock_position_client(
398 &self,
399 id: Xid,
400 r: Region,
401 _: u32,
402 _: bool,
403 ) -> xconnection::Result<()> {
404 let mut v = self.positions.take();
405 v.push((id, r));
406 self.positions.set(v);
407 Ok(())
408 }
409 }
410
411 test_cases! {
412 toggle_fullscreen;
413 args: (
414 n_clients: u32,
415 fullscreen: Option<Xid>,
416 target: Xid,
417 unmapped: &[Xid],
418 should_apply_layout: bool,
419 expected_positions: Vec<Xid>,
420 expected_maps: Vec<Xid>,
421 expected_unmaps: Vec<Xid>,
422 );
423
424 case: single_client_on => (1, None, 0, &[], false, vec![0], vec![], vec![]);
425 case: single_client_off => (1, Some(0), 0, &[], true, vec![], vec![], vec![]);
426 case: multiple_clients_on => (4, None, 1, &[], false, vec![1], vec![], vec![0, 2, 3]);
427 case: multiple_clients_off => (4, Some(1), 1, &[0, 2, 3], true, vec![], vec![0, 2, 3], vec![]);
428
429 body: {
430 let conn = RecordingXConn::init();
431 let ids: Vec<Xid> = (0..n_clients).collect();
432
433 let mut clients = Clients {
434 inner: ids.iter()
435 .map(|&id| {
436 let mut client = Client::new(&conn, id, 0, &[]);
437 client.mapped = true;
438 (id, client)
439 })
440 .collect(),
441 focused_client_id: None,
442 focused_border: 0xffffff.into(),
443 unfocused_border: 0x000000.into(),
444 };
445
446 let r = Region::new(0, 0, 1000, 800);
447 let expected_positions: Vec<_> = expected_positions.iter().map(|id| (*id, r)).collect();
448
449 for id in unmapped {
450 clients.modify(*id, |c| c.mapped = false);
451 }
452
453 if let Some(id) = fullscreen {
454 clients.modify(id, |c| c.fullscreen = true);
455 }
456
457 let events = clients.toggle_fullscreen(target, 42, &ids, r, &conn).unwrap();
458
459 assert_eq!(!events.is_empty(), should_apply_layout);
460 assert_eq!(conn.positions.take(), expected_positions);
461 assert_eq!(conn.maps.take(), expected_maps);
462 assert_eq!(conn.unmaps.take(), expected_unmaps);
463 }
464 }
465}