Files
barbar/crates/modules/src/workspaces.rs
T
2026-09-14 00:55:25 +01:00

145 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Workspaces module: renders workspaces 110 as cubes, coloured and scaled by
//! state — focused largest, existing mid, non-existent smallest. State arrives
//! as `HyprlandEvent`s from the hyprland service.
//!
//! Ported from the Quickshell `Workspaces.qml`: constant 10×15 slot per
//! workspace, corner radius 2.5, and the inactive cubes scaled about their
//! centre (0.85 existing / 0.75 absent) so the shrink never shifts the row.
use std::collections::BTreeSet;
use common::{BarModule, HyprlandEvent, HyprlandMsg, ModuleEffect, Service, Wire};
use iced::border;
use iced::theme::palette;
use iced::widget::{container, row, space};
use iced::{window, Element, Length, Task, Theme};
use serde::Deserialize;
use toml::Table;
/// How far each state's cube fades from the theme text colour toward the
/// theme background. Opaque (mixed, not alpha) so cubes stay solid over the
/// transparent bar instead of washing out.
const FADE_ACTIVE: f32 = 0.0;
const FADE_EXISTS: f32 = 0.55;
const FADE_ABSENT: f32 = 0.8;
/// Look of the workspace row, from `[modules.workspaces]`; every field
/// defaults to the value Quickshell's `Workspaces.qml` hardcodes, so an
/// absent table (or absent key) renders identically to before.
#[derive(Deserialize)]
#[serde(default)]
struct WorkspacesConfig {
/// Number of slots to render (workspaces `1..=count`).
count: i32,
/// Horizontal gap between slots in logical px.
gap: f32,
/// Slot width in logical px — every workspace keeps this slot, so the
/// unfocused shrink never moves the row.
slot_w: f32,
/// Slot height in logical px.
slot_h: f32,
/// Corner radius.
radius: f32,
/// Visual scale per state.
scale_active: f32,
scale_exists: f32,
scale_absent: f32,
}
impl Default for WorkspacesConfig {
fn default() -> Self {
Self {
count: 10,
gap: 6.0,
slot_w: 10.0,
slot_h: 15.0,
radius: 2.5,
scale_active: 1.0,
scale_exists: 0.85,
scale_absent: 0.75,
}
}
}
fn config_workspaces(table: Option<Table>) -> Option<WorkspacesConfig> {
let ws = table?.get("workspaces")?.clone();
ws.try_into().ok()
}
pub struct Workspaces {
cfg: WorkspacesConfig,
active: Option<i32>,
existing: BTreeSet<i32>,
}
impl Workspaces {
pub fn new(config: Option<Table>) -> Self {
Self {
cfg: config_workspaces(config).unwrap_or_default(),
active: None,
existing: BTreeSet::new(),
}
}
fn cube(&self, id: i32) -> Element<'_, Wire> {
let (fade, scale) = if self.active == Some(id) {
(FADE_ACTIVE, self.cfg.scale_active)
} else if self.existing.contains(&id) {
(FADE_EXISTS, self.cfg.scale_exists)
} else {
(FADE_ABSENT, self.cfg.scale_absent)
};
let radius = self.cfg.radius;
let bar = container(space())
.width(Length::Fixed(self.cfg.slot_w * scale))
.height(Length::Fixed(self.cfg.slot_h * scale))
.style(move |theme: &Theme| {
let p = theme.palette();
container::Style {
background: Some(palette::mix(p.text, p.background, fade).into()),
border: border::rounded(radius),
..Default::default()
}
});
container(bar)
.center_x(Length::Fixed(self.cfg.slot_w))
.center_y(Length::Fixed(self.cfg.slot_h))
.into()
}
}
impl BarModule for Workspaces {
fn id(&self) -> &'static str {
"workspaces"
}
fn view(&self, _window_id: Option<window::Id>) -> Element<'_, Wire> {
row((1..=self.cfg.count).map(|id| self.cube(id)))
.spacing(self.cfg.gap)
.into()
}
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
if let Some(event) = msg.downcast::<HyprlandEvent>() {
match event {
HyprlandEvent::WorkspaceChanged { id, .. } => self.active = Some(*id),
HyprlandEvent::WorkspaceAdded { id, .. } => {
self.existing.insert(*id);
}
HyprlandEvent::WorkspaceRemoved { id, .. } => {
self.existing.remove(id);
if self.active == Some(*id) {
self.active = None;
}
}
HyprlandEvent::ActiveWindow { .. } | HyprlandEvent::Monitor { .. } => {}
}
}
Task::none()
}
fn services(&self) -> Vec<Service> {
vec![HyprlandMsg::Events.key()]
}
}