fresh-start

This commit is contained in:
2026-08-31 22:51:49 +01:00
parent 81f242419e
commit cc5db9089a
8 changed files with 44 additions and 481 deletions
+43 -183
View File
@@ -1,72 +1,34 @@
//! Wayland layer-shell status bar built on `iced` + `iced_layershell`.
//! barbar — a Wayland layer-shell status bar built on `iced` + `iced_layershell`.
//!
//! Renders a full-width bar pinned to the top of the screen via the
//! `wlr-layer-shell` protocol. Optionally target a specific output by
//! passing its name as the first CLI argument.
mod bounds;
mod module;
mod modules;
use iced::widget::{container, text, Row};
use iced::{window, Alignment, Element, Length, Rectangle, Task, Theme};
use iced_layershell::daemon;
use iced_layershell::reexport::{Anchor, PopupAnchor, PopupGravity};
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
use iced::widget::{row, text};
use iced::{Alignment, Element, Length, Task, Theme};
use iced_layershell::application;
use iced_layershell::reexport::Anchor;
use iced_layershell::settings::{LayerShellSettings, StartMode, Settings};
use iced_layershell::to_layer_message;
use crate::module::{ModuleMsg, ModuleState, MODULES};
/// Height of the bar in logical pixels.
const BAR_HEIGHT: u32 = 36;
const POPUP_W: u32 = 150;
const POPUP_H: u32 = 100;
/// Optional width of the bar; `None` means "as wide as the output".
const BAR_WIDTH: Option<u32> = None;
/// Vertical gap (logical px) between the bar's bottom edge and the popup.
const POPUP_GAP: i32 = 32;
struct Bar {
/// Which button triggered the open popup? None = closed. Keyed by module label.
active_popup: Option<String>,
/// Surface ID of the currently open popup (for removal).
popup_id: Option<window::Id>,
/// Live module states, in registry order.
modules: Vec<Box<dyn ModuleState>>,
}
impl Default for Bar {
fn default() -> Self {
Self {
active_popup: None,
popup_id: None,
modules: MODULES.iter().map(|m| m.new_state()).collect(),
}
}
}
#[to_layer_message(multi)]
#[derive(Debug, Clone)]
enum Message {
Noop,
ModuleClicked(String),
BoundsFound(String, Rectangle),
ClosePopup(window::Id),
Module(String, Box<dyn ModuleMsg>),
}
fn namespace() -> String {
String::from("barbar")
}
fn main() -> Result<(), iced_layershell::Error> {
pub fn main() -> Result<(), iced_layershell::Error> {
// Target a specific output if one was given, otherwise the active one.
let start_mode = match std::env::args().nth(1) {
Some(output) => StartMode::TargetScreen(output),
None => StartMode::Active,
};
daemon(Bar::default, namespace, update, view)
application(Bar::default, namespace, update, view)
.style(style)
.subscription(subscription)
.settings(Settings {
layer_settings: LayerShellSettings {
size: Some((0, BAR_HEIGHT)),
size: Some((BAR_WIDTH.unwrap_or(0), BAR_HEIGHT)),
exclusive_zone: BAR_HEIGHT as i32,
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
start_mode,
@@ -77,149 +39,47 @@ fn main() -> Result<(), iced_layershell::Error> {
.run()
}
/// Folds module subscriptions into one daemon subscription.
fn subscription(bar: &Bar) -> iced::Subscription<Message> {
iced::Subscription::batch(bar.modules.iter().flat_map(|m| m.subscriptions()))
/// Application state. Empty for now; placeholders will evolve into
/// workspace/clock/system modules.
#[derive(Debug, Default)]
struct Bar;
/// Messages produced by user input, events and subscriptions.
#[to_layer_message]
#[derive(Debug, Clone)]
enum Message {
/// No-op used as a fallback target for pending subscriptions.
Noop,
}
fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
match msg {
Message::ModuleClicked(label) => {
// Toggle: close if already open, else open.
if bar.active_popup.as_deref() == Some(label.as_str()) {
return maybe_close_popup(bar);
}
// Query the widget tree for the module's actual laid-out bounds.
// The resulting `BoundsFound` message opens the popup there.
bounds::capture_bounds(label)
}
Message::BoundsFound(label, bounds) => {
bar.active_popup = Some(label.clone());
// Anchor rect = the button's bounds, extended below the bar so the
// `Bottom` anchor point (bottom-center) sits POPUP_GAP below the
// button's bottom edge. With `Bottom` gravity the popup grows
// downward from there, leaving a gap under the bar.
let (x, y, w, h) = (
bounds.x.round() as i32,
bounds.y.round() as i32,
bounds.width.round() as i32,
bounds.height.round() as i32,
);
let anchor_rect = (x, y, w, h + POPUP_GAP);
let settings = iced_layershell::actions::IcedNewPopupSettings::on_current_surface(
(POPUP_W, POPUP_H),
anchor_rect,
)
.anchor(PopupAnchor::Bottom)
.gravity(PopupGravity::Bottom);
let (id, task) = Message::popup_open(settings);
bar.popup_id = Some(id);
task
}
Message::Module(label, msg) => {
let state = bar
.modules
.iter_mut()
.find(|m| m.label() == label)
.expect("unknown module label");
state.handle(msg)
}
Message::ClosePopup(id) => {
if bar.popup_id == Some(id) {
bar.popup_id = None;
bar.active_popup = None;
}
Task::done(Message::RemoveWindow(id))
}
Message::Noop => Task::none(),
// Forward multi-window mutations to their internal handlers.
Message::AnchorChange { .. }
| Message::SetInputRegion { .. }
| Message::AnchorSizeChange { .. }
| Message::LayerChange { .. }
| Message::MarginChange { .. }
| Message::SizeChange { .. }
| Message::ExclusiveZoneChange { .. }
| Message::KeyboardInteractivityChange { .. }
| Message::NewBaseWindow { .. }
| Message::NewInputPanel { .. }
| Message::NewLayerShell { .. }
| Message::NewMenu { .. }
| Message::NewPopUp { .. }
| Message::RemoveWindow(_)
| Message::ForgetLastOutput
| Message::VirtualKeyboardPressed { .. } => Task::none(),
}
fn namespace() -> String {
String::from("barbar")
}
fn maybe_close_popup(bar: &mut Bar) -> Task<Message> {
if let Some(id) = bar.popup_id.take() {
bar.active_popup = None;
return Task::done(Message::RemoveWindow(id));
}
fn update(_bar: &mut Bar, _message: Message) -> Task<Message> {
Task::none()
}
/// Dispatch view by which surface is being rendered.
fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> {
if bar.popup_id == Some(id) {
popup_view(bar)
} else {
bar_view(bar)
}
}
/// Main bar widget tree.
fn bar_view(bar: &Bar) -> Element<'static, Message> {
let mut children: Vec<Element<Message>> = Vec::new();
for (i, module) in bar.modules.iter().enumerate() {
children.push(module.button());
if i < bar.modules.len() - 1 {
children.push(text("|").size(14).into());
}
}
container(
Row::from_vec(children)
.width(Length::Fill)
.align_y(Alignment::Center),
)
fn view(_bar: &Bar) -> Element<'_, Message> {
row![
text("workspaces").size(14),
text("|").size(14),
text("clock").size(14),
text("|").size(14),
text("system").size(14),
]
.spacing(12)
.padding(8)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
/// Popup widget tree. Rendered on its own floating LayerShell surface.
fn popup_view(bar: &Bar) -> Element<'static, Message> {
let label = bar
.active_popup
.as_ref()
.expect("popup open without active module");
let popup_id = bar.popup_id.expect("popup open without surface id");
let module = bar
.modules
.iter()
.find(|m| m.label() == label)
.expect("unknown module label");
module.popup(popup_id)
}
fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
use iced::theme::Style;
Style {
background_color: theme.palette().background,
text_color: theme.palette().text,
}
}
}