1use crate::{
3 Result,
4 bar::widgets::Widget,
5 core::{Context, TextStyle},
6};
7use penrose::{
8 Color,
9 core::{ClientSpace, State},
10 pure::geometry::Rect,
11 x::XConn,
12};
13
14const PADDING: u32 = 3;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FocusState {
19 Unfocused,
21 FocusedOnThisScreen,
23 FocusedOnOtherScreen,
25}
26
27impl FocusState {
28 pub fn focused(&self) -> bool {
30 matches!(self, Self::FocusedOnOtherScreen | Self::FocusedOnThisScreen)
31 }
32}
33
34pub trait WorkspacesUi {
36 #[allow(unused_variables)]
43 fn update_from_state<X>(
44 &mut self,
45 workspace_meta: &[WsMeta],
46 focused_tags: &[String],
47 state: &State<X>,
48 x: &X,
49 ) -> bool
50 where
51 X: XConn,
52 {
53 false
54 }
55
56 fn ui_tag(&self, workspace_meta: &WsMeta) -> String {
58 workspace_meta.tag.clone()
59 }
60
61 fn background_color(&self) -> Color;
63
64 fn colors_for_workspace(
70 &self,
71 workspace_meta: &WsMeta,
72 focus_state: FocusState,
73 screen_has_focus: bool,
74 ) -> (Color, Color);
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct DefaultUi {
80 fg_1: Color,
81 fg_2: Color,
82 bg_1: Color,
83 bg_2: Color,
84}
85
86impl DefaultUi {
87 fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
88 Self {
89 fg_1: style.fg,
90 fg_2: empty_fg.into(),
91 bg_1: highlight.into(),
92 bg_2: style.bg.unwrap_or_else(|| 0x000000.into()),
93 }
94 }
95}
96
97impl WorkspacesUi for DefaultUi {
98 fn background_color(&self) -> Color {
99 self.bg_2
100 }
101
102 fn colors_for_workspace(
103 &self,
104 &WsMeta { occupied, .. }: &WsMeta,
105 focus_state: FocusState,
106 screen_has_focus: bool,
107 ) -> (Color, Color) {
108 use FocusState::*;
109
110 match focus_state {
111 FocusedOnThisScreen if screen_has_focus && occupied => (self.fg_1, self.bg_1),
112 FocusedOnThisScreen if screen_has_focus => (self.fg_2, self.bg_1),
113 FocusedOnThisScreen => (self.fg_1, self.fg_2),
114 FocusedOnOtherScreen => (self.bg_1, self.fg_2),
115 Unfocused if occupied => (self.fg_1, self.bg_2),
116 Unfocused => (self.fg_2, self.bg_2),
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq)]
124pub struct WsMeta {
125 tag: String,
126 occupied: bool,
127 extent: (u32, u32),
128}
129
130impl WsMeta {
131 pub fn tag(&self) -> &str {
133 &self.tag
134 }
135
136 pub fn occupied(&self) -> bool {
138 self.occupied
139 }
140
141 fn from_state<X>(state: &State<X>) -> Vec<Self>
142 where
143 X: XConn,
144 {
145 state
146 .client_set
147 .ordered_workspaces()
148 .map(WsMeta::from)
149 .collect()
150 }
151}
152
153impl From<&ClientSpace> for WsMeta {
154 fn from(w: &ClientSpace) -> Self {
155 Self {
156 tag: w.tag().to_owned(),
157 occupied: !w.is_empty(),
158 extent: (0, 0),
159 }
160 }
161}
162
163fn focused_workspaces<X>(state: &State<X>) -> Vec<String>
164where
165 X: XConn,
166{
167 let mut indexed_screens: Vec<(usize, String)> = state
168 .client_set
169 .screens()
170 .map(|s| (s.index(), s.workspace.tag().to_owned()))
171 .collect();
172
173 indexed_screens.sort_by_key(|(ix, _)| *ix);
174
175 indexed_screens.into_iter().map(|(_, tag)| tag).collect()
176}
177
178pub type Workspaces = WorkspacesWidget<DefaultUi>;
180
181impl Workspaces {
182 pub fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
184 WorkspacesWidget::new_with_ui(DefaultUi::new(style, highlight, empty_fg))
185 }
186}
187
188#[derive(Debug, Clone, PartialEq)]
190pub struct WorkspacesWidget<U>
191where
192 U: WorkspacesUi,
193{
194 workspaces: Vec<WsMeta>,
195 focused_ws: Vec<String>, extent: Option<(u32, u32)>,
197 ui: U,
198 require_draw: bool,
199}
200
201impl<U> WorkspacesWidget<U>
202where
203 U: WorkspacesUi,
204{
205 pub fn new_with_ui(ui: U) -> Self {
207 Self {
208 workspaces: Vec::new(),
209 focused_ws: Vec::new(), extent: None,
211 ui,
212 require_draw: true,
213 }
214 }
215
216 fn raw_tags(&self) -> Vec<&str> {
217 self.workspaces.iter().map(|w| w.tag.as_ref()).collect()
218 }
219
220 fn update_from_state<X>(&mut self, state: &State<X>, x: &X)
221 where
222 X: XConn,
223 {
224 let focused_ws = focused_workspaces(state);
225 let wss = WsMeta::from_state(state);
226
227 let ui_updated = self.ui.update_from_state(&wss, &focused_ws, state, x);
228 let tags_changed = self.tags_changed(&wss);
229
230 if ui_updated
231 || tags_changed
232 || self.focused_ws != focused_ws
233 || self.occupied_changed(&wss)
234 {
235 self.focused_ws = focused_ws;
236 self.workspaces = wss;
237 self.require_draw = true;
238 self.extent = None;
239 }
240 }
241
242 fn tags_changed(&self, workspaces: &[WsMeta]) -> bool {
243 let new_tags: Vec<&str> = workspaces.iter().map(|w| w.tag.as_ref()).collect();
244
245 self.raw_tags() != new_tags
246 }
247
248 fn occupied_changed(&self, workspaces: &[WsMeta]) -> bool {
250 self.workspaces
251 .iter()
252 .zip(workspaces)
253 .any(|(l, r)| l.occupied != r.occupied)
254 }
255
256 fn ws_colors(&self, meta: &WsMeta, screen: usize, screen_has_focus: bool) -> (Color, Color) {
257 let focused = self.focused_ws.iter().any(|t| t == &meta.tag);
258 let focused_on_this_screen = match &self.focused_ws.get(screen) {
259 &Some(focused_tag) => &meta.tag == focused_tag,
260 None => false,
261 };
262
263 let state = match (focused, focused_on_this_screen) {
264 (false, _) => FocusState::Unfocused,
265 (_, true) => FocusState::FocusedOnThisScreen,
266 (true, false) => FocusState::FocusedOnOtherScreen,
267 };
268
269 self.ui.colors_for_workspace(meta, state, screen_has_focus)
270 }
271}
272
273impl<X, U> Widget<X> for WorkspacesWidget<U>
274where
275 X: XConn,
276 U: WorkspacesUi,
277{
278 fn draw(
279 &mut self,
280 ctx: &mut Context<'_>,
281 screen: usize,
282 screen_has_focus: bool,
283 w: u32,
284 h: u32,
285 ) -> Result<()> {
286 ctx.fill_rect(Rect::new(0, 0, w, h), self.ui.background_color())?;
287 ctx.translate(PADDING as i32, 0);
288 let (_, eh) = <Self as Widget<X>>::current_extent(self, ctx, h)?;
289
290 for ws in self.workspaces.iter() {
291 let (fg, bg) = self.ws_colors(ws, screen, screen_has_focus);
292 ctx.fill_rect(Rect::new(0, 0, ws.extent.0, h), bg)?;
293 ctx.draw_text(&self.ui.ui_tag(ws), h - eh, (PADDING, PADDING), fg)?;
294 ctx.translate(ws.extent.0 as i32, 0);
295 }
296
297 self.require_draw = false;
298
299 Ok(())
300 }
301
302 fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> {
303 match self.extent {
304 Some(extent) => Ok(extent),
305 None => {
306 let mut total = 0;
307 let mut h_max = 0;
308 for ws in self.workspaces.iter_mut() {
309 let (w, h) = ctx.text_extent(&self.ui.ui_tag(ws))?;
310 total += w + 2 * PADDING;
311 h_max = if h > h_max { h } else { h_max };
312 ws.extent = (w + 2 * PADDING, h);
313 }
314
315 let ext = (total + PADDING, h_max);
316 self.extent = Some(ext);
317
318 Ok(ext)
319 }
320 }
321 }
322
323 fn is_greedy(&self) -> bool {
324 false
325 }
326
327 fn require_draw(&self) -> bool {
328 self.require_draw
329 }
330
331 fn on_startup(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
332 self.update_from_state(state, x);
333
334 Ok(())
335 }
336
337 fn on_refresh(&mut self, state: &mut State<X>, x: &mut X) -> Result<()> {
338 self.update_from_state(state, x);
339
340 Ok(())
341 }
342}