145 lines
4.6 KiB
Rust
145 lines
4.6 KiB
Rust
//! Workspaces module: renders workspaces 1–10 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()]
|
||
}
|
||
}
|