fresh-start
This commit is contained in:
+43
-183
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user