msg type -> wire::push() -> update handle wire into module/service/ect

This commit is contained in:
2026-09-12 15:04:06 +01:00
parent 539c80cda9
commit 27ec2b90a6
24 changed files with 411 additions and 204 deletions
+4
View File
@@ -1,5 +1,7 @@
use iced::{window, Rectangle};
use crate::Wire;
/// 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.
@@ -12,4 +14,6 @@ pub enum ModuleEffect {
BoundsFound(String, Rectangle),
/// Request removal of the popup surface.
ClosePopup(window::Id),
/// Route a wire to its target (a module, a service inbox, or a topic).
Send(Wire),
}
+6 -2
View File
@@ -6,10 +6,14 @@
mod config;
mod effect;
mod messages;
mod module;
mod service;
mod wire;
pub use config::BarbarConfig;
pub use effect::ModuleEffect;
pub use module::{BarModule, ModuleMsg};
pub use service::{Service, ServiceEvent};
pub use messages::{ClockKind, ClockMsg, ClockPayload};
pub use module::BarModule;
pub use service::Service;
pub use wire::{Endpoint, Inbox, Namespace, Target, Wire};
+13
View File
@@ -0,0 +1,13 @@
//! Wire vocabulary: every message type that crosses a crate boundary.
//!
//! Laid out to mirror the crate that owns the behavior: `modules/*` are
//! messages a bar module sends or receives, `services/*` are messages a
//! service produces or accepts. Types live here so either side can name a
//! payload without depending on the other's crate; only the types are
//! shared, the behavior stays in the owning crate.
pub mod modules;
pub mod services;
pub use modules::*;
pub use services::*;
@@ -0,0 +1,10 @@
//! Clock module messages.
/// A module-local message for the clock (bar/popup interactions).
#[derive(Clone)]
pub enum ClockMsg {
/// Toggle the seconds suffix.
ToggleSeconds,
/// Open the big-time popup.
OpenPopup,
}
@@ -0,0 +1,5 @@
//! Messages owned by bar modules.
mod clock;
pub use clock::ClockMsg;
@@ -0,0 +1,27 @@
//! Clock service messages.
use crate::Service;
/// 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,
}
@@ -0,0 +1,5 @@
//! Messages owned by services.
mod datetime;
pub use datetime::{ClockKind, ClockPayload};
+5 -39
View File
@@ -1,56 +1,22 @@
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()
}
}
use crate::{ModuleEffect, Wire};
/// 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.
/// `Wire` boundary keeps the app ignorant of each module's message enum
/// the module downcasts whatever arrives.
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>;
fn view(&self, window_id: Option<window::Id>) -> Element<'_, Wire>;
/// Handles a routed message; may return app-level tasks (e.g. popups).
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect>;
fn update(&mut self, msg: Wire) -> Task<ModuleEffect>;
/// What services this module wants.
fn services(&self) -> Vec<crate::Service>;
-11
View File
@@ -1,16 +1,5 @@
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>,
}
+151
View File
@@ -0,0 +1,151 @@
use std::any::Any;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use iced::futures::channel::mpsc;
/// Which namespace an endpoint lives in. Module ids and service route keys
/// are separate namespaces, so a module and a service may share a name.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Namespace {
Module,
Service,
}
/// A wire's sender: a module id or a service route key.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Endpoint {
pub ns: Namespace,
pub name: &'static str,
}
impl Endpoint {
pub const fn module(name: &'static str) -> Self {
Self {
ns: Namespace::Module,
name,
}
}
pub const fn service(name: &'static str) -> Self {
Self {
ns: Namespace::Service,
name,
}
}
}
/// Where a wire goes. Core owns the routing rule for each variant.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Target {
/// One module, by id.
Module(&'static str),
/// One running service's inbox, by route key.
Service(&'static str),
/// Every module subscribed to a route key (a service's fan-out).
Topic(&'static str),
}
/// One typed message on the wire. Core routes by `to`; the receiver
/// downcasts `payload` to one of *its own* types. Sender and receiver keep
/// their own concrete types — nothing here enumerates them.
#[derive(Clone)]
pub struct Wire {
pub from: Endpoint,
pub to: Target,
pub payload: Arc<dyn Any + Send + Sync>,
}
impl Wire {
/// A module-local message: addressed straight back to one module.
pub fn module<T: Any + Send + Sync>(from: Endpoint, id: &'static str, msg: T) -> Self {
Self {
from,
to: Target::Module(id),
payload: Arc::new(msg),
}
}
/// Sent into a service's inbox.
pub fn service<T: Any + Send + Sync>(from: Endpoint, key: &'static str, msg: T) -> Self {
Self {
from,
to: Target::Service(key),
payload: Arc::new(msg),
}
}
/// Published by a service to every module subscribed to `key`.
pub fn topic<T: Any + Send + Sync>(from: Endpoint, key: &'static str, msg: T) -> Self {
Self {
from,
to: Target::Topic(key),
payload: Arc::new(msg),
}
}
/// Downcasts to the receiver's own message type.
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
self.payload.downcast_ref()
}
}
impl fmt::Debug for Wire {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Wire")
.field("from", &self.from)
.field("to", &self.to)
.field("payload", &self.payload.type_id())
.finish()
}
}
/// A service's inbound mailbox: modules send `Wire`s here, the service's
/// subscription task reads them. Cloneable; the receiver is taken once.
///
/// The `Arc<Mutex<Option<..>>>` lets `Bar` keep a clone and hand the same
/// value to `Subscription::run_with` on every rebuild without moving the
/// single-consumer receiver out of its reach.
#[derive(Clone)]
pub struct Inbox {
key: &'static str,
tx: mpsc::UnboundedSender<Wire>,
rx: Arc<Mutex<Option<mpsc::UnboundedReceiver<Wire>>>>,
}
impl Inbox {
/// Creates the channel backing a service's inbox.
pub fn new(key: &'static str) -> Self {
let (tx, rx) = mpsc::unbounded();
Self {
key,
tx,
rx: Arc::new(Mutex::new(Some(rx))),
}
}
/// The service's route key, used as the `Topic` it publishes under.
pub fn key(&self) -> &'static str {
self.key
}
/// Sends a wire into the service; `false` if the service is gone.
pub fn send(&self, wire: Wire) -> bool {
self.tx.unbounded_send(wire).is_ok()
}
/// Takes the receiver; the service stream calls this once, when iced
/// first starts it.
pub fn take(&self) -> Option<mpsc::UnboundedReceiver<Wire>> {
self.rx.lock().unwrap().take()
}
}
impl Hash for Inbox {
/// Identity for `Subscription::run_with`: the route key, so returning
/// the same service subscription each pass does not restart its stream.
fn hash<H: Hasher>(&self, state: &mut H) {
self.key.hash(state);
}
}