reorginise
This commit is contained in:
Generated
+1
@@ -1719,6 +1719,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
"common",
|
||||||
"iced",
|
"iced",
|
||||||
|
"service-datetime",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ chrono = "0.4"
|
|||||||
toml = "1.1"
|
toml = "1.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|
||||||
# Optimize all dependencies even in dev builds; workspace members keep dev
|
# Optimize all dependencies even in dev builds; workspace members keep dev
|
||||||
# defaults (opt-level 0) and release settings under --release.
|
# defaults (opt-level 0) and release settings under --release.
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use iced::{window, Rectangle};
|
||||||
|
|
||||||
|
/// What a module asks the app to do — the single effect vocabulary shared by
|
||||||
|
/// modules and app, so mapping module output into a `Message` is a plain
|
||||||
|
/// `.map(Message::Effect)` with no mirror enum.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ModuleEffect {
|
||||||
|
/// Toggle a popup for the given module id (element id anchors it).
|
||||||
|
RequestPopup(String, String),
|
||||||
|
/// A widget-tree pass reported a module's laid-out bounds; anchor the
|
||||||
|
/// popup there. App-internal, but one effect type keeps routing trivial.
|
||||||
|
BoundsFound(String, Rectangle),
|
||||||
|
/// Request removal of the popup surface.
|
||||||
|
ClosePopup(window::Id),
|
||||||
|
}
|
||||||
+6
-132
@@ -4,136 +4,10 @@
|
|||||||
//! layer-shell effects). `common` is the hub every other crate depends on,
|
//! layer-shell effects). `common` is the hub every other crate depends on,
|
||||||
//! so it must stay acyclic and pure.
|
//! so it must stay acyclic and pure.
|
||||||
|
|
||||||
use std::any::Any;
|
mod effect;
|
||||||
use std::error::Error;
|
mod module;
|
||||||
use std::fmt;
|
mod service;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use iced::window;
|
pub use effect::ModuleEffect;
|
||||||
use iced::{Element, Rectangle, Task};
|
pub use module::{BarModule, ModuleMsg};
|
||||||
|
pub use service::{Service, ServiceEvent};
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Service model
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// What a clock subscription wants: the tick interval and payload selector.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub enum ClockKind {
|
|
||||||
Mins,
|
|
||||||
Seconds,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clock tick payload: the value plus the kind that produced it.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct ClockPayload {
|
|
||||||
pub kind: ClockKind,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Route key a module subscribes with; `Clock(kind)` selects the ticker.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Service {
|
|
||||||
Clock(ClockKind),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A service event with a typed payload. The app only reads
|
|
||||||
/// `key()` for fan-out; the consuming module downcasts the payload.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ServicePayloadKind {
|
|
||||||
Clock(ClockPayload),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServicePayloadKind {
|
|
||||||
/// Route key for this event. One arm per kind — adding a service
|
|
||||||
/// touches only this match, never the routing code.
|
|
||||||
pub fn key(&self) -> Service {
|
|
||||||
match self {
|
|
||||||
Self::Clock(payload) => Service::Clock(payload.kind),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Erases the typed payload for the module to downcast.
|
|
||||||
pub fn into_payload(self) -> Arc<dyn Any + Send + Sync> {
|
|
||||||
match self {
|
|
||||||
Self::Clock(payload) => Arc::new(payload),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Module protocol
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// A module-local message, boxed with its owner's `id`. The app routes by
|
|
||||||
/// `id` and never names a module's message type; the module downcasts.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ModuleMsg {
|
|
||||||
pub id: &'static str,
|
|
||||||
pub payload: Arc<dyn Any + Send + Sync>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModuleMsg {
|
|
||||||
/// Wraps a module's own message; `id` must match its `BarModule::id`.
|
|
||||||
pub fn new<T: Any + Send + Sync>(id: &'static str, msg: T) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
payload: Arc::new(msg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Downcasts to the module's own message type.
|
|
||||||
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
|
|
||||||
self.payload.downcast_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for ModuleMsg {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("ModuleMsg")
|
|
||||||
.field("id", &self.id)
|
|
||||||
.field("payload", &self.payload.type_id())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a module asks the app to do — the single effect vocabulary shared by
|
|
||||||
/// modules and app, so mapping module output into a `Message` is a plain
|
|
||||||
/// `.map(Message::Effect)` with no mirror enum.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ModuleEffect {
|
|
||||||
/// Toggle a popup for the given module id (element id anchors it).
|
|
||||||
RequestPopup(String, String),
|
|
||||||
/// A widget-tree pass reported a module's laid-out bounds; anchor the
|
|
||||||
/// popup there. App-internal, but one effect type keeps routing trivial.
|
|
||||||
BoundsFound(String, Rectangle),
|
|
||||||
/// Request removal of the popup surface.
|
|
||||||
ClosePopup(window::Id),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A bar module. Object-safe; modules live in a `BTreeMap` keyed by id. The
|
|
||||||
/// `ModuleMsg` boundary keeps the app ignorant of each module's message enum.
|
|
||||||
pub trait BarModule: Send {
|
|
||||||
fn id(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Bar contents. When `window_id` is `Some`, this is the module's popup
|
|
||||||
/// surface and the module returns its popup contents instead.
|
|
||||||
fn view(&self, window_id: Option<window::Id>) -> Element<'_, ModuleMsg>;
|
|
||||||
|
|
||||||
/// Handles a routed message; may return app-level tasks (e.g. popups).
|
|
||||||
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect>;
|
|
||||||
|
|
||||||
/// What services this module wants.
|
|
||||||
fn services(&self) -> Vec<Service>;
|
|
||||||
|
|
||||||
/// Requested popup size when it's not the generic small menu.
|
|
||||||
fn popup_size(&self) -> Option<(u32, u32)> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Digest config from `module.{module-id}`. Error is boxed because this
|
|
||||||
/// runs through `dyn BarModule`; each impl keeps its own concrete error
|
|
||||||
/// internally and may surface it pre-box via its constructor instead.
|
|
||||||
fn config(&mut self, _config: toml::Table) -> Result<(), Box<dyn Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use iced::window;
|
||||||
|
use iced::{Element, Task};
|
||||||
|
|
||||||
|
use crate::ModuleEffect;
|
||||||
|
|
||||||
|
/// A module-local message, boxed with its owner's `id`. The app routes by
|
||||||
|
/// `id` and never names a module's message type; the module downcasts.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ModuleMsg {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub payload: Arc<dyn Any + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleMsg {
|
||||||
|
/// Wraps a module's own message; `id` must match its `BarModule::id`.
|
||||||
|
pub fn new<T: Any + Send + Sync>(id: &'static str, msg: T) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
payload: Arc::new(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downcasts to the module's own message type.
|
||||||
|
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
|
||||||
|
self.payload.downcast_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ModuleMsg {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("ModuleMsg")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("payload", &self.payload.type_id())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bar module. Object-safe; modules live in a `BTreeMap` keyed by id. The
|
||||||
|
/// `ModuleMsg` boundary keeps the app ignorant of each module's message enum.
|
||||||
|
pub trait BarModule: Send {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Bar contents. When `window_id` is `Some`, this is the module's popup
|
||||||
|
/// surface and the module returns its popup contents instead.
|
||||||
|
fn view(&self, window_id: Option<window::Id>) -> Element<'_, ModuleMsg>;
|
||||||
|
|
||||||
|
/// Handles a routed message; may return app-level tasks (e.g. popups).
|
||||||
|
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect>;
|
||||||
|
|
||||||
|
/// What services this module wants.
|
||||||
|
fn services(&self) -> Vec<crate::Service>;
|
||||||
|
|
||||||
|
/// Requested popup size when it's not the generic small menu.
|
||||||
|
fn popup_size(&self) -> Option<(u32, u32)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Digest config from `module.{module-id}`. Error is boxed because this
|
||||||
|
/// runs through `dyn BarModule`; each impl keeps its own concrete error
|
||||||
|
/// internally and may surface it pre-box via its constructor instead.
|
||||||
|
fn config(&mut self, _config: toml::Table) -> Result<(), Box<dyn Error>> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Opaque route key identifying a service subscription. Services name their
|
||||||
|
/// own keys, so `common` never enumerates them and adding a service never
|
||||||
|
/// edits this crate.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct Service(pub &'static str);
|
||||||
|
|
||||||
|
/// A service event: the route key plus an erased typed payload. The app reads
|
||||||
|
/// `key` for fan-out; the consuming module downcasts `payload`.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ServiceEvent {
|
||||||
|
pub key: Service,
|
||||||
|
pub payload: Arc<dyn Any + Send + Sync>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use iced::window;
|
||||||
|
|
||||||
|
use common::{BarModule, Service};
|
||||||
|
|
||||||
|
/// Application state: module registry + popup bookkeeping.
|
||||||
|
pub(crate) struct Bar {
|
||||||
|
/// Module whose popup is open; None = closed.
|
||||||
|
pub(crate) active_popup: Option<String>,
|
||||||
|
/// Module registry keyed by stable module id.
|
||||||
|
pub(crate) modules: BTreeMap<&'static str, Box<dyn BarModule>>,
|
||||||
|
/// Surface id of the open popup.
|
||||||
|
pub(crate) popup_id: Option<window::Id>,
|
||||||
|
/// Fan-out routing table: module id -> services it wants.
|
||||||
|
pub(crate) routes: BTreeMap<&'static str, Vec<Service>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bar {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
// Registry owns construction: adding a module never edits this file.
|
||||||
|
let modules: BTreeMap<&'static str, Box<dyn BarModule>> =
|
||||||
|
modules::all().into_iter().map(|m| (m.id(), m)).collect();
|
||||||
|
let routes = modules
|
||||||
|
.iter()
|
||||||
|
.map(|(id, module)| (*id, module.services()))
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
active_popup: None,
|
||||||
|
modules,
|
||||||
|
popup_id: None,
|
||||||
|
routes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-218
@@ -4,119 +4,37 @@
|
|||||||
//! `wlr-layer-shell`. Optional first CLI arg = target output name.
|
//! `wlr-layer-shell`. Optional first CLI arg = target output name.
|
||||||
//! Clicking a module's button spawns a LayerShell popup anchored to it.
|
//! Clicking a module's button spawns a LayerShell popup anchored to it.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use iced::widget::{column, container, text};
|
|
||||||
use iced::{window, Alignment, Element, Length, Subscription, Task, Theme};
|
|
||||||
use iced_layershell::daemon;
|
use iced_layershell::daemon;
|
||||||
use iced_layershell::reexport::Anchor;
|
use iced_layershell::reexport::Anchor;
|
||||||
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
|
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
|
||||||
use iced_layershell::to_layer_message;
|
|
||||||
|
|
||||||
use common::{BarModule, ModuleEffect, ModuleMsg, Service as ModuleService, ServicePayloadKind};
|
|
||||||
use services::IntoSubscription;
|
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod msg;
|
||||||
mod popup;
|
mod popup;
|
||||||
|
mod subscription;
|
||||||
|
mod update;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
pub(crate) use app::Bar;
|
||||||
|
pub(crate) use msg::{Message, Msg};
|
||||||
|
|
||||||
/// Height of the bar in logical pixels.
|
/// Height of the bar in logical pixels.
|
||||||
const BAR_HEIGHT: u32 = 36;
|
const BAR_HEIGHT: u32 = 36;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Types
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Application state: module registry + popup bookkeeping.
|
|
||||||
struct Bar {
|
|
||||||
/// Module whose popup is open; None = closed.
|
|
||||||
active_popup: Option<String>,
|
|
||||||
/// Module registry keyed by stable module id.
|
|
||||||
modules: BTreeMap<&'static str, Box<dyn BarModule>>,
|
|
||||||
/// Surface id of the open popup.
|
|
||||||
popup_id: Option<window::Id>,
|
|
||||||
/// Fan-out routing table: module id -> services it wants.
|
|
||||||
routes: BTreeMap<&'static str, Vec<ModuleService>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bar {
|
|
||||||
fn new() -> Self {
|
|
||||||
// Registry owns construction: adding a module never edits this file.
|
|
||||||
let modules: BTreeMap<&'static str, Box<dyn BarModule>> =
|
|
||||||
modules::all().into_iter().map(|m| (m.id(), m)).collect();
|
|
||||||
let routes = modules
|
|
||||||
.iter()
|
|
||||||
.map(|(id, module)| (*id, module.services()))
|
|
||||||
.collect();
|
|
||||||
Self {
|
|
||||||
active_popup: None,
|
|
||||||
modules,
|
|
||||||
popup_id: None,
|
|
||||||
routes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Messages
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Synchronous input: module traffic, subscription payloads, window events.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) enum Msg {
|
|
||||||
/// Routed module message; dispatch by `id`, module downcasts.
|
|
||||||
Module(ModuleMsg),
|
|
||||||
/// Service event; fan out by route key.
|
|
||||||
Subscription(ServicePayloadKind),
|
|
||||||
/// The compositor confirmed a window closed. The popup is really gone.
|
|
||||||
WindowClosed(window::Id),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// iced needs one `Message` type. `to_layer_message` must sit here: it
|
|
||||||
/// injects the layer-shell effect variants + their `TryInto` impl.
|
|
||||||
#[to_layer_message(multi)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) enum Message {
|
|
||||||
Event(Msg),
|
|
||||||
/// Effects. `common::ModuleEffect` is the one effect vocabulary, so
|
|
||||||
/// mapping module output is a plain `.map(Message::Effect)`.
|
|
||||||
Effect(ModuleEffect),
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Application wiring
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn namespace() -> String {
|
|
||||||
String::from("barbar")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Route subscriptions + window-close events (popup really destroyed).
|
|
||||||
/// Each module's wants go through the services registry, so adding a
|
|
||||||
/// service never edits this file.
|
|
||||||
fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
|
|
||||||
let route_sub = Subscription::batch(
|
|
||||||
bar.routes
|
|
||||||
.values()
|
|
||||||
.flat_map(|wants| wants.iter())
|
|
||||||
.map(|&service| {
|
|
||||||
service
|
|
||||||
.into_subscription()
|
|
||||||
.map(|event| Message::Event(Msg::Subscription(event)))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id)));
|
|
||||||
Subscription::batch(vec![route_sub, close_events])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), iced_layershell::Error> {
|
fn main() -> Result<(), iced_layershell::Error> {
|
||||||
let start_mode = match std::env::args().nth(1) {
|
let start_mode = match std::env::args().nth(1) {
|
||||||
Some(output) => StartMode::TargetScreen(output),
|
Some(output) => StartMode::TargetScreen(output),
|
||||||
None => StartMode::Active,
|
None => StartMode::Active,
|
||||||
};
|
};
|
||||||
|
|
||||||
daemon(Bar::new, namespace, update, view)
|
daemon(
|
||||||
.style(style)
|
app::Bar::new,
|
||||||
.subscription(gather_subscriptions)
|
subscription::namespace,
|
||||||
|
update::update,
|
||||||
|
view::view,
|
||||||
|
)
|
||||||
|
.style(view::style)
|
||||||
|
.subscription(subscription::gather_subscriptions)
|
||||||
.settings(Settings {
|
.settings(Settings {
|
||||||
layer_settings: LayerShellSettings {
|
layer_settings: LayerShellSettings {
|
||||||
size: Some((0, BAR_HEIGHT)),
|
size: Some((0, BAR_HEIGHT)),
|
||||||
@@ -129,123 +47,3 @@ fn main() -> Result<(), iced_layershell::Error> {
|
|||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Update
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
|
|
||||||
match msg {
|
|
||||||
Message::Event(event) => handle_event(bar, event),
|
|
||||||
Message::Effect(effect) => handle_effect(bar, effect),
|
|
||||||
// Layer-shell variants injected by `to_layer_message`.
|
|
||||||
_ => Task::none(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies an input event to state; effects come back as `Task`s.
|
|
||||||
fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
|
|
||||||
match event {
|
|
||||||
Msg::Module(m) => match bar.modules.get_mut(m.id) {
|
|
||||||
// Modules return protocol tasks; wrap effects into messages.
|
|
||||||
Some(module) => module.update(m).map(Message::Effect),
|
|
||||||
None => Task::none(),
|
|
||||||
},
|
|
||||||
|
|
||||||
Msg::Subscription(event) => {
|
|
||||||
// Fan out by route key; the payload is opaque here, the module
|
|
||||||
// downcasts it. New kinds touch only `SubscriptionPayloadKind`.
|
|
||||||
let key = event.key();
|
|
||||||
let payload = event.into_payload();
|
|
||||||
bar.modules
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|(id, _)| {
|
|
||||||
bar.routes
|
|
||||||
.get(*id)
|
|
||||||
.is_some_and(|kinds| kinds.contains(&key))
|
|
||||||
})
|
|
||||||
.map(|(_, module)| {
|
|
||||||
module
|
|
||||||
.update(ModuleMsg {
|
|
||||||
id: module.id(),
|
|
||||||
payload: payload.clone(),
|
|
||||||
})
|
|
||||||
.map(Message::Effect)
|
|
||||||
})
|
|
||||||
.fold(Task::none(), |acc, t| acc.chain(t))
|
|
||||||
}
|
|
||||||
|
|
||||||
Msg::WindowClosed(id) => {
|
|
||||||
// Popup gone; forget it. `popup_id` stays set until this event
|
|
||||||
// so the popup content (not the bar) renders during teardown.
|
|
||||||
if bar.popup_id == Some(id) {
|
|
||||||
bar.popup_id = None;
|
|
||||||
bar.active_popup = None;
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs an effect; may set up bar state for it (e.g. which popup is open).
|
|
||||||
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
|
|
||||||
match effect {
|
|
||||||
ModuleEffect::RequestPopup(module_id, element_id) => {
|
|
||||||
// Toggle: close if already open for this module, else open.
|
|
||||||
if bar.active_popup.as_deref() == Some(module_id.as_str()) {
|
|
||||||
popup::close_popup(bar)
|
|
||||||
} else {
|
|
||||||
popup::capture_bounds(module_id, element_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ModuleEffect::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds),
|
|
||||||
|
|
||||||
ModuleEffect::ClosePopup(id) => {
|
|
||||||
// Request removal only; keep popup state so the popup content
|
|
||||||
// renders until `WindowClosed` confirms it's gone.
|
|
||||||
Task::done(Message::RemoveWindow(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> {
|
|
||||||
if bar.popup_id == Some(id) {
|
|
||||||
popup::view(bar) // popup surface
|
|
||||||
} else {
|
|
||||||
bar_view(bar) // main bar surface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each module renders itself; the bar lays them out in a row.
|
|
||||||
fn bar_view(bar: &Bar) -> Element<'_, Message> {
|
|
||||||
let mut children: Vec<Element<Message>> = Vec::new();
|
|
||||||
|
|
||||||
for (i, (_, module)) in bar.modules.iter().enumerate() {
|
|
||||||
children.push(module.view(None).map(|m| Message::Event(Msg::Module(m))));
|
|
||||||
if i < bar.modules.len() - 1 {
|
|
||||||
children.push(text("|").size(14).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container(
|
|
||||||
column(children)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.align_x(Alignment::Center),
|
|
||||||
)
|
|
||||||
.padding(8)
|
|
||||||
.align_x(Alignment::Center)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Style
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
|
||||||
use iced::theme::Style;
|
|
||||||
Style {
|
|
||||||
background_color: theme.palette().background,
|
|
||||||
text_color: theme.palette().text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use iced::window;
|
||||||
|
use iced_layershell::to_layer_message;
|
||||||
|
|
||||||
|
use common::{ModuleEffect, ModuleMsg, ServiceEvent};
|
||||||
|
|
||||||
|
/// Synchronous input: module traffic, subscription payloads, window events.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) enum Msg {
|
||||||
|
/// Routed module message; dispatch by `id`, module downcasts.
|
||||||
|
Module(ModuleMsg),
|
||||||
|
/// Service event; fan out by route key.
|
||||||
|
Subscription(ServiceEvent),
|
||||||
|
/// The compositor confirmed a window closed. The popup is really gone.
|
||||||
|
WindowClosed(window::Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iced needs one `Message` type. `to_layer_message` must sit here: it
|
||||||
|
/// injects the layer-shell effect variants + their `TryInto` impl.
|
||||||
|
#[to_layer_message(multi)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) enum Message {
|
||||||
|
Event(Msg),
|
||||||
|
/// Effects. `common::ModuleEffect` is the one effect vocabulary, so
|
||||||
|
/// mapping module output is a plain `.map(Message::Effect)`.
|
||||||
|
Effect(ModuleEffect),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `to_layer_message` generates `popup_open` as a fn private to this module;
|
||||||
|
/// re-expose it for the popup surface code, which lives elsewhere.
|
||||||
|
pub(crate) fn popup_open(
|
||||||
|
settings: iced_layershell::actions::IcedNewPopupSettings,
|
||||||
|
) -> (
|
||||||
|
iced_layershell::reexport::IcedId,
|
||||||
|
iced_layershell::reexport::Task<Message>,
|
||||||
|
) {
|
||||||
|
Message::popup_open(settings)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ use iced_layershell::reexport::{PopupAnchor, PopupGravity};
|
|||||||
|
|
||||||
use common::ModuleEffect;
|
use common::ModuleEffect;
|
||||||
|
|
||||||
|
use crate::msg::popup_open;
|
||||||
use crate::{Bar, Message, Msg};
|
use crate::{Bar, Message, Msg};
|
||||||
|
|
||||||
/// Default (small menu) popup size.
|
/// Default (small menu) popup size.
|
||||||
@@ -45,7 +46,7 @@ pub fn open_popup(bar: &mut Bar, module_id: String, bounds: Rectangle) -> Task<M
|
|||||||
.anchor(PopupAnchor::Bottom)
|
.anchor(PopupAnchor::Bottom)
|
||||||
.gravity(PopupGravity::Bottom);
|
.gravity(PopupGravity::Bottom);
|
||||||
|
|
||||||
let (id, task) = Message::popup_open(settings);
|
let (id, task) = popup_open(settings);
|
||||||
bar.popup_id = Some(id);
|
bar.popup_id = Some(id);
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use iced::{window, Subscription};
|
||||||
|
|
||||||
|
use services::IntoSubscription;
|
||||||
|
|
||||||
|
use crate::app::Bar;
|
||||||
|
use crate::msg::{Message, Msg};
|
||||||
|
|
||||||
|
pub(crate) fn namespace() -> String {
|
||||||
|
String::from("barbar")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route subscriptions + window-close events (popup really destroyed).
|
||||||
|
/// Each module's wants go through the services registry, so adding a
|
||||||
|
/// service never edits this file.
|
||||||
|
pub(crate) fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
|
||||||
|
let route_sub = Subscription::batch(
|
||||||
|
bar.routes
|
||||||
|
.values()
|
||||||
|
.flat_map(|wants| wants.iter())
|
||||||
|
.map(|&service| {
|
||||||
|
service
|
||||||
|
.into_subscription()
|
||||||
|
.map(|event| Message::Event(Msg::Subscription(event)))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id)));
|
||||||
|
Subscription::batch(vec![route_sub, close_events])
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use iced::Task;
|
||||||
|
|
||||||
|
use common::{ModuleEffect, ModuleMsg};
|
||||||
|
|
||||||
|
use crate::app::Bar;
|
||||||
|
use crate::msg::{Message, Msg};
|
||||||
|
use crate::popup;
|
||||||
|
|
||||||
|
pub(crate) fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
|
||||||
|
match msg {
|
||||||
|
Message::Event(event) => handle_event(bar, event),
|
||||||
|
Message::Effect(effect) => handle_effect(bar, effect),
|
||||||
|
// Layer-shell variants injected by `to_layer_message`.
|
||||||
|
_ => Task::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies an input event to state; effects come back as `Task`s.
|
||||||
|
fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
|
||||||
|
match event {
|
||||||
|
Msg::Module(m) => match bar.modules.get_mut(m.id) {
|
||||||
|
// Modules return protocol tasks; wrap effects into messages.
|
||||||
|
Some(module) => module.update(m).map(Message::Effect),
|
||||||
|
None => Task::none(),
|
||||||
|
},
|
||||||
|
|
||||||
|
Msg::Subscription(event) => {
|
||||||
|
// Fan out by route key; the payload is opaque here, the module
|
||||||
|
// downcasts it. New services never touch this file.
|
||||||
|
let key = event.key;
|
||||||
|
let payload = event.payload;
|
||||||
|
bar.modules
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(id, _)| {
|
||||||
|
bar.routes
|
||||||
|
.get(*id)
|
||||||
|
.is_some_and(|kinds| kinds.contains(&key))
|
||||||
|
})
|
||||||
|
.map(|(_, module)| {
|
||||||
|
module
|
||||||
|
.update(ModuleMsg {
|
||||||
|
id: module.id(),
|
||||||
|
payload: payload.clone(),
|
||||||
|
})
|
||||||
|
.map(Message::Effect)
|
||||||
|
})
|
||||||
|
.fold(Task::none(), |acc, t| acc.chain(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
Msg::WindowClosed(id) => {
|
||||||
|
// Popup gone; forget it. `popup_id` stays set until this event
|
||||||
|
// so the popup content (not the bar) renders during teardown.
|
||||||
|
if bar.popup_id == Some(id) {
|
||||||
|
bar.popup_id = None;
|
||||||
|
bar.active_popup = None;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs an effect; may set up bar state for it (e.g. which popup is open).
|
||||||
|
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
|
||||||
|
match effect {
|
||||||
|
ModuleEffect::RequestPopup(module_id, element_id) => {
|
||||||
|
// Toggle: close if already open for this module, else open.
|
||||||
|
if bar.active_popup.as_deref() == Some(module_id.as_str()) {
|
||||||
|
popup::close_popup(bar)
|
||||||
|
} else {
|
||||||
|
popup::capture_bounds(module_id, element_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleEffect::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds),
|
||||||
|
|
||||||
|
ModuleEffect::ClosePopup(id) => {
|
||||||
|
// Request removal only; keep popup state so the popup content
|
||||||
|
// renders until `WindowClosed` confirms it's gone.
|
||||||
|
Task::done(Message::RemoveWindow(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use iced::widget::{column, container, text};
|
||||||
|
use iced::{window, Alignment, Element, Length, Theme};
|
||||||
|
|
||||||
|
use crate::app::Bar;
|
||||||
|
use crate::msg::{Message, Msg};
|
||||||
|
use crate::popup;
|
||||||
|
|
||||||
|
pub(crate) fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> {
|
||||||
|
if bar.popup_id == Some(id) {
|
||||||
|
popup::view(bar) // popup surface
|
||||||
|
} else {
|
||||||
|
bar_view(bar) // main bar surface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each module renders itself; the bar lays them out in a row.
|
||||||
|
fn bar_view(bar: &Bar) -> Element<'_, Message> {
|
||||||
|
let mut children: Vec<Element<Message>> = Vec::new();
|
||||||
|
|
||||||
|
for (i, (_, module)) in bar.modules.iter().enumerate() {
|
||||||
|
children.push(module.view(None).map(|m| Message::Event(Msg::Module(m))));
|
||||||
|
if i < bar.modules.len() - 1 {
|
||||||
|
children.push(text("|").size(14).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container(
|
||||||
|
column(children)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
)
|
||||||
|
.padding(8)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
||||||
|
use iced::theme::Style;
|
||||||
|
Style {
|
||||||
|
background_color: theme.palette().background,
|
||||||
|
text_color: theme.palette().text,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../../common" }
|
common = { path = "../../common" }
|
||||||
|
service_datetime = { package = "service-datetime", path = "../../services/datetime" }
|
||||||
iced = { workspace = true }
|
iced = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|||||||
@@ -2,118 +2,6 @@
|
|||||||
//! Left-click toggles the seconds suffix; right-click opens a popup with
|
//! Left-click toggles the seconds suffix; right-click opens a popup with
|
||||||
//! the current time in large text.
|
//! the current time in large text.
|
||||||
|
|
||||||
use iced::widget::{container, mouse_area, text};
|
mod module;
|
||||||
use iced::{Element, Task};
|
|
||||||
|
|
||||||
use common::{BarModule, ClockKind, ClockPayload, ModuleEffect, ModuleMsg, Service};
|
pub use module::{Clock, ClockError, ClockMsg};
|
||||||
|
|
||||||
/// Big-text popup size (logical px).
|
|
||||||
const POPUP_W: u32 = 360;
|
|
||||||
const POPUP_H: u32 = 160;
|
|
||||||
|
|
||||||
/// Errors digesting the `module.clock` config table.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ClockError {
|
|
||||||
/// Config value was present but not a bool.
|
|
||||||
#[error("module.clock.{0} must be a bool")]
|
|
||||||
NotBool(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A module-local message for the clock.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ClockMsg {
|
|
||||||
/// Toggle the seconds suffix.
|
|
||||||
ToggleSeconds,
|
|
||||||
/// Open the big-time popup.
|
|
||||||
OpenPopup,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Clock {
|
|
||||||
value: String,
|
|
||||||
show_seconds: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clock {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
value: "--:--:--".to_string(),
|
|
||||||
show_seconds: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display string: HH:MM:SS, or HH:MM when seconds are hidden.
|
|
||||||
fn shown(&self) -> String {
|
|
||||||
if self.show_seconds {
|
|
||||||
self.value.clone()
|
|
||||||
} else {
|
|
||||||
self.value
|
|
||||||
.split(':') // [hh, mm, ss]
|
|
||||||
.take(2)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(":")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Clock {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BarModule for Clock {
|
|
||||||
fn id(&self) -> &'static str {
|
|
||||||
"clock"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, ModuleMsg> {
|
|
||||||
match window_id {
|
|
||||||
// Popup surface: current time in large text.
|
|
||||||
Some(_) => container(text(&self.value).size(56)).padding(20).into(),
|
|
||||||
// Bar surface: clickable time. Container carries the module id
|
|
||||||
// so the popup can anchor to these bounds.
|
|
||||||
None => container(
|
|
||||||
mouse_area(text(self.shown()).size(16))
|
|
||||||
.on_press(ModuleMsg::new(self.id(), ClockMsg::ToggleSeconds))
|
|
||||||
.on_right_press(ModuleMsg::new(self.id(), ClockMsg::OpenPopup)),
|
|
||||||
)
|
|
||||||
.id(iced::widget::Id::from(self.id()))
|
|
||||||
.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect> {
|
|
||||||
// Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`.
|
|
||||||
if let Some(payload) = msg.downcast::<ClockPayload>() {
|
|
||||||
self.value = payload.value.clone();
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
match msg.downcast::<ClockMsg>() {
|
|
||||||
Some(ClockMsg::ToggleSeconds) => {
|
|
||||||
self.show_seconds = !self.show_seconds;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
|
|
||||||
self.id().to_string(),
|
|
||||||
self.id().to_string(),
|
|
||||||
)),
|
|
||||||
None => Task::none(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn services(&self) -> Vec<Service> {
|
|
||||||
vec![Service::Clock(ClockKind::Seconds)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn popup_size(&self) -> Option<(u32, u32)> {
|
|
||||||
Some((POPUP_W, POPUP_H))
|
|
||||||
}
|
|
||||||
fn config(&mut self, config: toml::Table) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if let toml::Value::Boolean(e) = config["format"] {
|
|
||||||
if e {
|
|
||||||
self.show_seconds = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use iced::widget::{container, mouse_area, text};
|
||||||
|
use iced::{Element, Task};
|
||||||
|
|
||||||
|
use common::{BarModule, ModuleEffect, ModuleMsg, Service};
|
||||||
|
use service_datetime::{ClockKind, ClockPayload};
|
||||||
|
|
||||||
|
/// Big-text popup size (logical px).
|
||||||
|
const POPUP_W: u32 = 360;
|
||||||
|
const POPUP_H: u32 = 160;
|
||||||
|
|
||||||
|
/// Errors digesting the `module.clock` config table.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ClockError {
|
||||||
|
/// Config value was present but not a bool.
|
||||||
|
#[error("module.clock.{0} must be a bool")]
|
||||||
|
NotBool(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A module-local message for the clock.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ClockMsg {
|
||||||
|
/// Toggle the seconds suffix.
|
||||||
|
ToggleSeconds,
|
||||||
|
/// Open the big-time popup.
|
||||||
|
OpenPopup,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Clock {
|
||||||
|
value: String,
|
||||||
|
show_seconds: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clock {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
value: "--:--:--".to_string(),
|
||||||
|
show_seconds: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display string: HH:MM:SS, or HH:MM when seconds are hidden.
|
||||||
|
fn shown(&self) -> String {
|
||||||
|
if self.show_seconds {
|
||||||
|
self.value.clone()
|
||||||
|
} else {
|
||||||
|
self.value
|
||||||
|
.split(':') // [hh, mm, ss]
|
||||||
|
.take(2)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Clock {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarModule for Clock {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"clock"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, ModuleMsg> {
|
||||||
|
match window_id {
|
||||||
|
// Popup surface: current time in large text.
|
||||||
|
Some(_) => container(text(&self.value).size(56)).padding(20).into(),
|
||||||
|
// Bar surface: clickable time. Container carries the module id
|
||||||
|
// so the popup can anchor to these bounds.
|
||||||
|
None => container(
|
||||||
|
mouse_area(text(self.shown()).size(16))
|
||||||
|
.on_press(ModuleMsg::new(self.id(), ClockMsg::ToggleSeconds))
|
||||||
|
.on_right_press(ModuleMsg::new(self.id(), ClockMsg::OpenPopup)),
|
||||||
|
)
|
||||||
|
.id(iced::widget::Id::from(self.id()))
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect> {
|
||||||
|
// Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`.
|
||||||
|
if let Some(payload) = msg.downcast::<ClockPayload>() {
|
||||||
|
self.value = payload.value.clone();
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
match msg.downcast::<ClockMsg>() {
|
||||||
|
Some(ClockMsg::ToggleSeconds) => {
|
||||||
|
self.show_seconds = !self.show_seconds;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
|
||||||
|
self.id().to_string(),
|
||||||
|
self.id().to_string(),
|
||||||
|
)),
|
||||||
|
None => Task::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn services(&self) -> Vec<Service> {
|
||||||
|
vec![ClockKind::Seconds.key()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popup_size(&self) -> Option<(u32, u32)> {
|
||||||
|
Some((POPUP_W, POPUP_H))
|
||||||
|
}
|
||||||
|
fn config(&mut self, config: toml::Table) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if let toml::Value::Boolean(e) = config["format"] {
|
||||||
|
if e {
|
||||||
|
self.show_seconds = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use common::Service;
|
||||||
|
|
||||||
|
/// Route-key namespace owned by this service; keys are `"clock.<kind>"`.
|
||||||
|
pub const NAMESPACE: &str = "clock.";
|
||||||
|
|
||||||
|
/// What a clock subscription wants: the tick interval and payload selector.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum ClockKind {
|
||||||
|
Mins,
|
||||||
|
Seconds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClockKind {
|
||||||
|
/// Route key a module subscribes under to receive this kind's ticks.
|
||||||
|
pub fn key(self) -> Service {
|
||||||
|
Service(match self {
|
||||||
|
Self::Mins => "clock.mins",
|
||||||
|
Self::Seconds => "clock.seconds",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clock tick payload: the value plus the kind that produced it.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct ClockPayload {
|
||||||
|
pub kind: ClockKind,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
@@ -1,73 +1,11 @@
|
|||||||
//! Clock service: emits a `ServicePayloadKind` event whenever a clock kind's
|
//! Clock service: emits a `ServiceEvent` whenever a clock kind's displayed
|
||||||
//! displayed value changes (minute or second rollover).
|
//! value changes (minute or second rollover).
|
||||||
//!
|
//!
|
||||||
//! The ticker knows nothing about the app's `Message` type; it produces
|
//! The ticker knows nothing about the app's `Message` type; it produces
|
||||||
//! protocol events from `common` that core routes to subscribed modules.
|
//! protocol events from `common` that core routes to subscribed modules.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
mod kind;
|
||||||
|
mod ticker;
|
||||||
|
|
||||||
use iced::{futures::SinkExt, Subscription};
|
pub use kind::{ClockKind, ClockPayload, NAMESPACE};
|
||||||
|
pub use ticker::ClockTicker;
|
||||||
use common::{ClockKind, ClockPayload, ServicePayloadKind};
|
|
||||||
|
|
||||||
/// Emits a payload only when a kind's displayed value changes (minute or
|
|
||||||
/// second rollover). Drive `tick` once per second.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ClockTicker {
|
|
||||||
last: HashMap<ClockKind, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClockTicker {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscription that emits `ServicePayloadKind::Clock` on every change.
|
|
||||||
pub fn run() -> Subscription<ServicePayloadKind> {
|
|
||||||
Subscription::run_with((), |_| {
|
|
||||||
iced::stream::channel(
|
|
||||||
0,
|
|
||||||
|mut sender: iced::futures::channel::mpsc::Sender<ServicePayloadKind>| async move {
|
|
||||||
let mut clock = ClockTicker::new();
|
|
||||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
for payload in clock.tick() {
|
|
||||||
if sender
|
|
||||||
.send(ServicePayloadKind::Clock(payload))
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a payload per kind whose value changed since the last tick.
|
|
||||||
/// The second that rolls into a new minute yields `[Seconds, Mins]`.
|
|
||||||
pub fn tick(&mut self) -> Vec<ClockPayload> {
|
|
||||||
let now = chrono::Local::now();
|
|
||||||
[ClockKind::Seconds, ClockKind::Mins]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|kind| {
|
|
||||||
let value = now
|
|
||||||
.format(match kind {
|
|
||||||
ClockKind::Mins => "%H:%M",
|
|
||||||
ClockKind::Seconds => "%H:%M:%S",
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
match self.last.get(&kind) {
|
|
||||||
Some(prev) if *prev == value => None,
|
|
||||||
_ => {
|
|
||||||
self.last.insert(kind, value.clone());
|
|
||||||
Some(ClockPayload { kind, value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use iced::{futures::SinkExt, Subscription};
|
||||||
|
|
||||||
|
use common::ServiceEvent;
|
||||||
|
|
||||||
|
use crate::kind::{ClockKind, ClockPayload};
|
||||||
|
|
||||||
|
/// Emits a payload only when a kind's displayed value changes (minute or
|
||||||
|
/// second rollover). Drive `tick` once per second.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ClockTicker {
|
||||||
|
last: HashMap<ClockKind, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClockTicker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscription that emits a `ServiceEvent` on every change.
|
||||||
|
pub fn run() -> Subscription<ServiceEvent> {
|
||||||
|
Subscription::run_with((), |_| {
|
||||||
|
iced::stream::channel(
|
||||||
|
0,
|
||||||
|
|mut sender: iced::futures::channel::mpsc::Sender<ServiceEvent>| async move {
|
||||||
|
let mut clock = ClockTicker::new();
|
||||||
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
for payload in clock.tick() {
|
||||||
|
let event = ServiceEvent {
|
||||||
|
key: payload.kind.key(),
|
||||||
|
payload: Arc::new(payload),
|
||||||
|
};
|
||||||
|
if sender.send(event).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a payload per kind whose value changed since the last tick.
|
||||||
|
/// The second that rolls into a new minute yields `[Seconds, Mins]`.
|
||||||
|
pub fn tick(&mut self) -> Vec<ClockPayload> {
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
[ClockKind::Seconds, ClockKind::Mins]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|kind| {
|
||||||
|
let value = now
|
||||||
|
.format(match kind {
|
||||||
|
ClockKind::Mins => "%H:%M",
|
||||||
|
ClockKind::Seconds => "%H:%M:%S",
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
match self.last.get(&kind) {
|
||||||
|
Some(prev) if *prev == value => None,
|
||||||
|
_ => {
|
||||||
|
self.last.insert(kind, value.clone());
|
||||||
|
Some(ClockPayload { kind, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use iced::Subscription;
|
use iced::Subscription;
|
||||||
|
|
||||||
use common::{Service, ServicePayloadKind};
|
use common::{Service, ServiceEvent};
|
||||||
|
|
||||||
/// Conversion from a route key to the subscription backing it.
|
/// Conversion from a route key to the subscription backing it.
|
||||||
///
|
///
|
||||||
@@ -12,13 +12,16 @@ use common::{Service, ServicePayloadKind};
|
|||||||
/// both `Service` and `Subscription` are foreign to this crate — the orphan
|
/// both `Service` and `Subscription` are foreign to this crate — the orphan
|
||||||
/// rule blocks a foreign trait (`From`) on a foreign type (`Subscription`).
|
/// rule blocks a foreign trait (`From`) on a foreign type (`Subscription`).
|
||||||
pub trait IntoSubscription {
|
pub trait IntoSubscription {
|
||||||
fn into_subscription(self) -> Subscription<ServicePayloadKind>;
|
fn into_subscription(self) -> Subscription<ServiceEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoSubscription for Service {
|
impl IntoSubscription for Service {
|
||||||
fn into_subscription(self) -> Subscription<ServicePayloadKind> {
|
fn into_subscription(self) -> Subscription<ServiceEvent> {
|
||||||
match self {
|
match self.0 {
|
||||||
Service::Clock(_) => service_datetime::ClockTicker::run(),
|
key if key.starts_with(service_datetime::NAMESPACE) => {
|
||||||
|
service_datetime::ClockTicker::run()
|
||||||
|
}
|
||||||
|
_ => Subscription::none(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user