step 5: live Hyprland workspace buttons via EventStream

This commit is contained in:
Breadway 2026-05-17 08:34:31 +08:00
parent 542cbae939
commit 241e3bc2cb
4 changed files with 109 additions and 10 deletions

View file

@ -1 +1,50 @@
use futures_lite::StreamExt;
use gtk4::prelude::*;
use hyprland::{
data::{Workspace, Workspaces},
event_listener::{Event, EventStream},
prelude::*,
shared::WorkspaceId,
};
use relm4::ComponentSender;
use crate::AppInput;
pub fn spawn_watcher(sender: ComponentSender<crate::App>) {
relm4::spawn(async move {
if let Ok(ws) = Workspaces::get_async().await {
sender.input(AppInput::WorkspaceList(ws.to_vec()));
}
if let Ok(active) = Workspace::get_active_async().await {
sender.input(AppInput::ActiveWorkspace(active.id));
}
let mut stream = EventStream::new();
while let Some(Ok(event)) = stream.next().await {
match event {
Event::WorkspaceChanged(data) => {
sender.input(AppInput::ActiveWorkspace(data.id));
}
Event::WorkspaceAdded(_) | Event::WorkspaceDeleted(_) => {
if let Ok(ws) = Workspaces::get_async().await {
sender.input(AppInput::WorkspaceList(ws.to_vec()));
}
}
_ => {}
}
}
});
}
pub fn make_button(id: WorkspaceId, name: &str, active: WorkspaceId) -> gtk4::Button {
let btn = gtk4::Button::with_label(name);
btn.add_css_class("workspace-btn");
if id == active {
btn.add_css_class("active");
}
btn.connect_clicked(move |_| {
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
let _ = Dispatch::call(DispatchType::Workspace(WorkspaceIdentifierWithSpecial::Id(id)));
});
btn
}

View file

@ -4,14 +4,27 @@ mod theme;
use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use hyprland::data::Workspace;
use hyprland::shared::WorkspaceId;
use relm4::prelude::*;
struct App;
pub struct App {
workspaces: Vec<Workspace>,
active_ws: WorkspaceId,
// Stored handle so update() can manipulate the live widget directly.
workspace_box: gtk4::Box,
}
#[relm4::component]
#[derive(Debug)]
pub enum AppInput {
WorkspaceList(Vec<Workspace>),
ActiveWorkspace(WorkspaceId),
}
#[relm4::component(pub)]
impl SimpleComponent for App {
type Init = ();
type Input = ();
type Input = AppInput;
type Output = ();
view! {
@ -24,11 +37,13 @@ impl SimpleComponent for App {
#[wrap(Some)]
set_start_widget = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 4,
set_spacing: 0,
set_margin_start: 8,
gtk::Label {
set_label: "1 2 3",
#[name = "workspace_box"]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 4,
}
},
@ -54,7 +69,7 @@ impl SimpleComponent for App {
fn init(
_: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
root.init_layer_shell();
root.set_layer(Layer::Top);
@ -63,15 +78,48 @@ impl SimpleComponent for App {
root.set_anchor(Edge::Right, true);
root.set_exclusive_zone(32);
let model = App;
// Placeholder until view_output! gives us the real handle.
let mut model = App {
workspaces: vec![],
active_ws: 1,
workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
};
let widgets = view_output!();
// Swap in the actual widget so update() can reach it.
model.workspace_box = widgets.workspace_box.clone();
theme::apply();
bar::workspaces::spawn_watcher(sender);
ComponentParts { model, widgets }
}
fn update(&mut self, _: Self::Input, _: ComponentSender<Self>) {}
fn update(&mut self, msg: Self::Input, _: ComponentSender<Self>) {
match msg {
AppInput::WorkspaceList(list) => {
let mut sorted = list;
sorted.sort_by_key(|w| w.id);
self.workspaces = sorted;
}
AppInput::ActiveWorkspace(id) => {
self.active_ws = id;
}
}
self.rebuild_buttons();
}
}
impl App {
fn rebuild_buttons(&self) {
while let Some(child) = self.workspace_box.first_child() {
self.workspace_box.remove(&child);
}
for ws in &self.workspaces {
self.workspace_box
.append(&bar::workspaces::make_button(ws.id, &ws.name, self.active_ws));
}
}
}
fn main() {