reorginise
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-228
@@ -4,248 +4,46 @@
|
||||
//! `wlr-layer-shell`. Optional first CLI arg = target output name.
|
||||
//! 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::reexport::Anchor;
|
||||
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 subscription;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
pub(crate) use app::Bar;
|
||||
pub(crate) use msg::{Message, Msg};
|
||||
|
||||
/// Height of the bar in logical pixels.
|
||||
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> {
|
||||
let start_mode = match std::env::args().nth(1) {
|
||||
Some(output) => StartMode::TargetScreen(output),
|
||||
None => StartMode::Active,
|
||||
};
|
||||
|
||||
daemon(Bar::new, namespace, update, view)
|
||||
.style(style)
|
||||
.subscription(gather_subscriptions)
|
||||
.settings(Settings {
|
||||
layer_settings: LayerShellSettings {
|
||||
size: Some((0, BAR_HEIGHT)),
|
||||
exclusive_zone: BAR_HEIGHT as i32,
|
||||
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
|
||||
start_mode,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.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),
|
||||
daemon(
|
||||
app::Bar::new,
|
||||
subscription::namespace,
|
||||
update::update,
|
||||
view::view,
|
||||
)
|
||||
.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,
|
||||
}
|
||||
.style(view::style)
|
||||
.subscription(subscription::gather_subscriptions)
|
||||
.settings(Settings {
|
||||
layer_settings: LayerShellSettings {
|
||||
size: Some((0, BAR_HEIGHT)),
|
||||
exclusive_zone: BAR_HEIGHT as i32,
|
||||
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
|
||||
start_mode,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -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 crate::msg::popup_open;
|
||||
use crate::{Bar, Message, Msg};
|
||||
|
||||
/// 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)
|
||||
.gravity(PopupGravity::Bottom);
|
||||
|
||||
let (id, task) = Message::popup_open(settings);
|
||||
let (id, task) = popup_open(settings);
|
||||
bar.popup_id = Some(id);
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user