use crate::{
bar::widgets::Widget,
core::{Context, TextStyle},
Result,
};
use penrose::{
core::{ClientSpace, State},
pure::geometry::Rect,
x::XConn,
Color,
};
const PADDING: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusState {
Unfocused,
FocusedOnThisScreen,
FocusedOnOtherScreen,
}
impl FocusState {
pub fn focused(&self) -> bool {
matches!(self, Self::FocusedOnOtherScreen | Self::FocusedOnThisScreen)
}
}
pub trait WorkspacesUi {
#[allow(unused_variables)]
fn update_from_state<X>(
&mut self,
workspace_meta: &[WsMeta],
focused_tags: &[String],
state: &State<X>,
x: &X,
) -> bool
where
X: XConn,
{
false
}
fn ui_tag(&self, workspace_meta: &WsMeta) -> String {
workspace_meta.tag.clone()
}
fn background_color(&self) -> Color;
fn colors_for_workspace(
&self,
workspace_meta: &WsMeta,
focus_state: FocusState,
screen_has_focus: bool,
) -> (Color, Color);
}
#[derive(Debug, Clone, PartialEq)]
pub struct DefaultUi {
fg_1: Color,
fg_2: Color,
bg_1: Color,
bg_2: Color,
}
impl DefaultUi {
fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
Self {
fg_1: style.fg,
fg_2: empty_fg.into(),
bg_1: highlight.into(),
bg_2: style.bg.unwrap_or_else(|| 0x000000.into()),
}
}
}
impl WorkspacesUi for DefaultUi {
fn background_color(&self) -> Color {
self.bg_2
}
fn colors_for_workspace(
&self,
&WsMeta { occupied, .. }: &WsMeta,
focus_state: FocusState,
screen_has_focus: bool,
) -> (Color, Color) {
use FocusState::*;
match focus_state {
FocusedOnThisScreen if screen_has_focus && occupied => (self.fg_1, self.bg_1),
FocusedOnThisScreen if screen_has_focus => (self.fg_2, self.bg_1),
FocusedOnThisScreen => (self.fg_1, self.fg_2),
FocusedOnOtherScreen => (self.bg_1, self.fg_2),
Unfocused if occupied => (self.fg_1, self.bg_2),
Unfocused => (self.fg_2, self.bg_2),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct WsMeta {
tag: String,
occupied: bool,
extent: (u32, u32),
}
impl WsMeta {
pub fn tag(&self) -> &str {
&self.tag
}
pub fn occupied(&self) -> bool {
self.occupied
}
fn from_state<X>(state: &State<X>) -> Vec<Self>
where
X: XConn,
{
state
.client_set
.ordered_workspaces()
.map(WsMeta::from)
.collect()
}
}
impl From<&ClientSpace> for WsMeta {
fn from(w: &ClientSpace) -> Self {
Self {
tag: w.tag().to_owned(),
occupied: !w.is_empty(),
extent: (0, 0),
}
}
}
fn focused_workspaces<X>(state: &State<X>) -> Vec<String>
where
X: XConn,
{
let mut indexed_screens: Vec<(usize, String)> = state
.client_set
.screens()
.map(|s| (s.index(), s.workspace.tag().to_owned()))
.collect();
indexed_screens.sort_by_key(|(ix, _)| *ix);
indexed_screens.into_iter().map(|(_, tag)| tag).collect()
}
pub type Workspaces = WorkspacesWidget<DefaultUi>;
impl Workspaces {
pub fn new(style: TextStyle, highlight: impl Into<Color>, empty_fg: impl Into<Color>) -> Self {
WorkspacesWidget::new_with_ui(DefaultUi::new(style, highlight, empty_fg))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorkspacesWidget<U>
where
U: WorkspacesUi,
{
workspaces: Vec<WsMeta>,
focused_ws: Vec<String>, extent: Option<(u32, u32)>,
ui: U,
require_draw: bool,
}
impl<U> WorkspacesWidget<U>
where
U: WorkspacesUi,
{
pub fn new_with_ui(ui: U) -> Self {
Self {
workspaces: Vec::new(),
focused_ws: Vec::new(), extent: None,
ui,
require_draw: true,
}
}
fn raw_tags(&self) -> Vec<&str> {
self.workspaces.iter().map(|w| w.tag.as_ref()).collect()
}
fn update_from_state<X>(&mut self, state: &State<X>, x: &X)
where
X: XConn,
{
let focused_ws = focused_workspaces(state);
let wss = WsMeta::from_state(state);
let ui_updated = self.ui.update_from_state(&wss, &focused_ws, state, x);
let tags_changed = self.tags_changed(&wss);
if ui_updated || tags_changed {
self.require_draw = true;
self.extent = None;
} else if self.focused_ws != focused_ws || self.occupied_changed(&wss) {
self.require_draw = true;
}
self.focused_ws = focused_ws;
self.workspaces = wss;
}
fn tags_changed(&self, workspaces: &[WsMeta]) -> bool {
let new_tags: Vec<&str> = workspaces.iter().map(|w| w.tag.as_ref()).collect();
self.raw_tags() == new_tags
}
fn occupied_changed(&self, workspaces: &[WsMeta]) -> bool {
self.workspaces
.iter()
.zip(workspaces)
.any(|(l, r)| l.occupied != r.occupied)
}
fn ws_colors(&self, meta: &WsMeta, screen: usize, screen_has_focus: bool) -> (Color, Color) {
let focused = self.focused_ws.iter().any(|t| t == &meta.tag);
let focused_on_this_screen = match &self.focused_ws.get(screen) {
&Some(focused_tag) => &meta.tag == focused_tag,
None => false,
};
let state = match (focused, focused_on_this_screen) {
(false, _) => FocusState::Unfocused,
(_, true) => FocusState::FocusedOnThisScreen,
(true, false) => FocusState::FocusedOnOtherScreen,
};
self.ui.colors_for_workspace(meta, state, screen_has_focus)
}
}
impl<X, U> Widget<X> for WorkspacesWidget<U>
where
X: XConn,
U: WorkspacesUi,
{
fn draw(
&mut self,
ctx: &mut Context<'_>,
screen: usize,
screen_has_focus: bool,
w: u32,
h: u32,
) -> Result<()> {
ctx.fill_rect(Rect::new(0, 0, w, h), self.ui.background_color())?;
ctx.translate(PADDING as i32, 0);
let (_, eh) = <Self as Widget<X>>::current_extent(self, ctx, h)?;
for ws in self.workspaces.iter() {
let (fg, bg) = self.ws_colors(ws, screen, screen_has_focus);
ctx.fill_rect(Rect::new(0, 0, ws.extent.0, h), bg)?;
ctx.draw_text(&self.ui.ui_tag(ws), h - eh, (PADDING, PADDING), fg)?;
ctx.translate(ws.extent.0 as i32, 0);
}
self.require_draw = false;
Ok(())
}
fn current_extent(&mut self, ctx: &mut Context<'_>, _h: u32) -> Result<(u32, u32)> {
match self.extent {
Some(extent) => Ok(extent),
None => {
let mut total = 0;
let mut h_max = 0;
for ws in self.workspaces.iter_mut() {
let (w, h) = ctx.text_extent(&self.ui.ui_tag(ws))?;
total += w + 2 * PADDING;
h_max = if h > h_max { h } else { h_max };
ws.extent = (w + 2 * PADDING, h);
}
let ext = (total + PADDING, h_max);
self.extent = Some(ext);
Ok(ext)
}
}
}
fn is_greedy(&self) -> bool {
false
}
fn require_draw(&self) -> bool {
self.require_draw
}
fn on_startup(&mut self, state: &mut State<X>, x: &X) -> Result<()> {
self.update_from_state(state, x);
Ok(())
}
fn on_refresh(&mut self, state: &mut State<X>, x: &X) -> Result<()> {
self.update_from_state(state, x);
Ok(())
}
}