hyprland workspace, service
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
//! 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::widget::{container, row, space};
|
||||
use iced::{window, Color, Element, Length, Task};
|
||||
use serde::Deserialize;
|
||||
use toml::Table;
|
||||
|
||||
/// Focused workspace (gruvbox base10 / `Colors.primaryText`).
|
||||
const ACTIVE: Color = Color::from_rgb(0.9216, 0.8588, 0.6980);
|
||||
/// Workspace exists but is not focused (base06 / `Colors.textSecondary`).
|
||||
const EXISTS: Color = Color::from_rgb(0.4863, 0.4353, 0.3922);
|
||||
/// Workspace does not exist (base04 / `Colors.textDisabled`).
|
||||
const ABSENT: Color = Color::from_rgb(0.3137, 0.2863, 0.2706);
|
||||
|
||||
/// 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 (color, scale) = if self.active == Some(id) {
|
||||
(ACTIVE, self.cfg.scale_active)
|
||||
} else if self.existing.contains(&id) {
|
||||
(EXISTS, self.cfg.scale_exists)
|
||||
} else {
|
||||
(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 |_| container::Style {
|
||||
background: Some(color.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()]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user