reorginise

This commit is contained in:
2026-09-10 02:08:45 +01:00
parent 8696425590
commit bdec970b62
20 changed files with 593 additions and 548 deletions
+15
View File
@@ -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
View File
@@ -4,136 +4,10 @@
//! layer-shell effects). `common` is the hub every other crate depends on,
//! so it must stay acyclic and pure.
use std::any::Any;
use std::error::Error;
use std::fmt;
use std::sync::Arc;
mod effect;
mod module;
mod service;
use iced::window;
use iced::{Element, Rectangle, Task};
// ---------------------------------------------------------------------------
// 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(())
}
}
pub use effect::ModuleEffect;
pub use module::{BarModule, ModuleMsg};
pub use service::{Service, ServiceEvent};
+69
View File
@@ -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(())
}
}
+16
View File
@@ -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>,
}