slop
This commit is contained in:
+1
-1
@@ -4,5 +4,5 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool"] }
|
iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool", "advanced"] }
|
||||||
iced_layershell = { version = "0.19", default-features = false }
|
iced_layershell = { version = "0.19", default-features = false }
|
||||||
+288
-37
@@ -3,32 +3,102 @@
|
|||||||
//! Renders a full-width bar pinned to the top of the screen via the
|
//! Renders a full-width bar pinned to the top of the screen via the
|
||||||
//! `wlr-layer-shell` protocol. Optionally target a specific output by
|
//! `wlr-layer-shell` protocol. Optionally target a specific output by
|
||||||
//! passing its name as the first CLI argument.
|
//! passing its name as the first CLI argument.
|
||||||
|
//!
|
||||||
|
//! Clicking any button spawns a separate LayerShell popup surface anchored
|
||||||
|
//! to that button's position on the bar.
|
||||||
|
|
||||||
use iced::widget::{row, text};
|
use iced::advanced::widget as advanced_widget;
|
||||||
use iced::{Alignment, Element, Length, Task, Theme};
|
use iced::widget::{column, container, mouse_area, text};
|
||||||
use iced_layershell::application;
|
use iced::{window, Alignment, Element, Length, Rectangle, Task, Theme};
|
||||||
use iced_layershell::reexport::Anchor;
|
use iced_layershell::daemon;
|
||||||
|
use iced_layershell::reexport::{Anchor, PopupAnchor, PopupGravity};
|
||||||
use iced_layershell::settings::{LayerShellSettings, StartMode, Settings};
|
use iced_layershell::settings::{LayerShellSettings, StartMode, Settings};
|
||||||
use iced_layershell::to_layer_message;
|
use iced_layershell::to_layer_message;
|
||||||
|
|
||||||
/// Height of the bar in logical pixels.
|
/// Height of the bar in logical pixels.
|
||||||
const BAR_HEIGHT: u32 = 36;
|
const BAR_HEIGHT: u32 = 36;
|
||||||
|
|
||||||
/// Optional width of the bar; `None` means "as wide as the output".
|
/// Popup dimensions.
|
||||||
const BAR_WIDTH: Option<u32> = None;
|
const POPUP_W: u32 = 150;
|
||||||
|
const POPUP_H: u32 = 100;
|
||||||
|
|
||||||
pub fn main() -> Result<(), iced_layershell::Error> {
|
/// Vertical gap (logical px) between the bar's bottom edge and the popup.
|
||||||
// Target a specific output if one was given, otherwise the active one.
|
const POPUP_GAP: i32 = 32;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Identifies which bar module triggered an action.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
enum Module {
|
||||||
|
Workspace,
|
||||||
|
Clock,
|
||||||
|
System,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module {
|
||||||
|
const ALL: &'static [Self] = &[Self::Workspace, Self::Clock, Self::System];
|
||||||
|
|
||||||
|
fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Workspace => "workspaces",
|
||||||
|
Self::Clock => "clock",
|
||||||
|
Self::System => "system",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable widget-tree id used to look up the laid-out bounds of the
|
||||||
|
/// module's clickable area.
|
||||||
|
fn id(&self) -> iced::widget::Id {
|
||||||
|
iced::widget::Id::new(self.label())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Application state. Tracks which popup is open and shared module data.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Bar {
|
||||||
|
/// Which button triggered the open popup? None = closed.
|
||||||
|
active_popup: Option<Module>,
|
||||||
|
/// Surface ID of the currently open popup (for removal).
|
||||||
|
popup_id: Option<window::Id>,
|
||||||
|
/// Example workspace list populated when workspace menu opens.
|
||||||
|
workspaces: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages produced by user input, events and subscriptions.
|
||||||
|
#[to_layer_message(multi)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
Noop,
|
||||||
|
/// A bar module was clicked; opens or toggles a popup.
|
||||||
|
ModuleClicked(Module),
|
||||||
|
/// Internal: the widget-tree layout pass reported the laid-out bounds of
|
||||||
|
/// a module's clickable area; the popup is anchored there.
|
||||||
|
BoundsFound(Module, Rectangle),
|
||||||
|
/// Closes the currently open popup.
|
||||||
|
ClosePopup(window::Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Application wiring
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn namespace() -> String {
|
||||||
|
String::from("barbar")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), iced_layershell::Error> {
|
||||||
let start_mode = match std::env::args().nth(1) {
|
let start_mode = match std::env::args().nth(1) {
|
||||||
Some(output) => StartMode::TargetScreen(output),
|
Some(output) => StartMode::TargetScreen(output),
|
||||||
None => StartMode::Active,
|
None => StartMode::Active,
|
||||||
};
|
};
|
||||||
|
|
||||||
application(Bar::default, namespace, update, view)
|
daemon(Bar::default, namespace, update, view)
|
||||||
.style(style)
|
.style(style)
|
||||||
.settings(Settings {
|
.settings(Settings {
|
||||||
layer_settings: LayerShellSettings {
|
layer_settings: LayerShellSettings {
|
||||||
size: Some((BAR_WIDTH.unwrap_or(0), BAR_HEIGHT)),
|
size: Some((0, BAR_HEIGHT)),
|
||||||
exclusive_zone: BAR_HEIGHT as i32,
|
exclusive_zone: BAR_HEIGHT as i32,
|
||||||
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
|
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
|
||||||
start_mode,
|
start_mode,
|
||||||
@@ -39,43 +109,224 @@ pub fn main() -> Result<(), iced_layershell::Error> {
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Application state. Empty for now; placeholders will evolve into
|
// ---------------------------------------------------------------------------
|
||||||
/// workspace/clock/system modules.
|
// Update
|
||||||
#[derive(Debug, Default)]
|
// ---------------------------------------------------------------------------
|
||||||
struct Bar;
|
|
||||||
|
|
||||||
/// Messages produced by user input, events and subscriptions.
|
fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
|
||||||
#[to_layer_message]
|
match msg {
|
||||||
#[derive(Debug, Clone)]
|
Message::ModuleClicked(module) => {
|
||||||
enum Message {
|
// Toggle: close if already open, else open.
|
||||||
/// No-op used as a fallback target for pending subscriptions.
|
if bar.active_popup == Some(module) {
|
||||||
Noop,
|
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.
|
||||||
|
capture_bounds(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::BoundsFound(module, bounds) => {
|
||||||
|
bar.active_popup = Some(module);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Seed example data for the workspace menu.
|
||||||
|
if module == Module::Workspace {
|
||||||
|
bar.workspaces = vec!["1".into(), "2".into(), "3".into(), "4".into()];
|
||||||
|
}
|
||||||
|
|
||||||
|
task
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
/// Helper: closes the popup if one is open.
|
||||||
String::from("barbar")
|
fn maybe_close_popup(bar: &mut Bar) -> Task<Message> {
|
||||||
}
|
if let Some(id) = bar.popup_id.take() {
|
||||||
|
bar.active_popup = None;
|
||||||
fn update(_bar: &mut Bar, _message: Message) -> Task<Message> {
|
return Task::done(Message::RemoveWindow(id));
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(_bar: &Bar) -> Element<'_, Message> {
|
/// Runs a widget-tree [`Operation`] that captures the laid-out bounds of the
|
||||||
row![
|
/// given module's clickable area, then reports them via `BoundsFound`.
|
||||||
text("workspaces").size(14),
|
fn capture_bounds(module: Module) -> Task<Message> {
|
||||||
text("|").size(14),
|
let target = module.id();
|
||||||
text("clock").size(14),
|
|
||||||
text("|").size(14),
|
struct FindBounds {
|
||||||
text("system").size(14),
|
target: iced::widget::Id,
|
||||||
|
found: Option<Rectangle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl advanced_widget::Operation<Rectangle> for FindBounds {
|
||||||
|
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn advanced_widget::Operation<Rectangle>)) {
|
||||||
|
// Delegate traversal so widgets can visit their children; the
|
||||||
|
// container id is matched in `container` above.
|
||||||
|
operate(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container(&mut self, id: Option<&iced::widget::Id>, bounds: Rectangle) {
|
||||||
|
if self.found.is_none() && id == Some(&self.target) {
|
||||||
|
self.found = Some(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) -> advanced_widget::operation::Outcome<Rectangle> {
|
||||||
|
match self.found {
|
||||||
|
Some(bounds) => advanced_widget::operation::Outcome::Some(bounds),
|
||||||
|
None => advanced_widget::operation::Outcome::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advanced_widget::operate(FindBounds {
|
||||||
|
target,
|
||||||
|
found: None,
|
||||||
|
})
|
||||||
|
.map(move |bounds| Message::BoundsFound(module, bounds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// View — routed per-surface by window ID
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Dispatch view by which surface is being rendered.
|
||||||
|
fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> {
|
||||||
|
// The bar is always created first. Any subsequently opened popup has a
|
||||||
|
// different IcedId we can distinguish. We compare against the bar's
|
||||||
|
// known ID (stored implicitly as "first window").
|
||||||
|
if bar.popup_id == Some(id) {
|
||||||
|
// This is the popup surface.
|
||||||
|
popup_view(bar)
|
||||||
|
} else {
|
||||||
|
// This is the main bar surface.
|
||||||
|
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 Module::ALL.iter().enumerate() {
|
||||||
|
children.push(button(*module));
|
||||||
|
if i < Module::ALL.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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standalone button wrapped in MouseArea for click capture. The container
|
||||||
|
/// carries the module's id so a widget-tree [`Operation`] can find its
|
||||||
|
/// laid-out bounds for popup anchoring.
|
||||||
|
fn button(module: Module) -> Element<'static, Message> {
|
||||||
|
container(mouse_area(text(module.label()).size(14)).on_press(Message::ModuleClicked(module)))
|
||||||
|
.id(module.id())
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Popup widget tree. Rendered on its own floating LayerShell surface.
|
||||||
|
fn popup_view<'a>(bar: &'a Bar) -> Element<'a, Message> {
|
||||||
|
let module = bar.active_popup.unwrap();
|
||||||
|
let popup_id = bar.popup_id.unwrap();
|
||||||
|
let label = module.label();
|
||||||
|
|
||||||
|
// Build list items based on which module was clicked.
|
||||||
|
let items: Vec<Element<Message>> = match module {
|
||||||
|
Module::Workspace => bar
|
||||||
|
.workspaces
|
||||||
|
.iter()
|
||||||
|
.map(|ws| popup_item(ws.clone(), popup_id))
|
||||||
|
.collect(),
|
||||||
|
Module::Clock => vec![
|
||||||
|
popup_item("Time format", popup_id),
|
||||||
|
popup_item("Date display", popup_id),
|
||||||
|
],
|
||||||
|
Module::System => vec![
|
||||||
|
popup_item("Brightness", popup_id),
|
||||||
|
popup_item("Volume", popup_id),
|
||||||
|
popup_item("Power", popup_id),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
column![
|
||||||
|
text(format!("{} menu", label))
|
||||||
|
.size(14)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
column(items).spacing(1),
|
||||||
|
mouse_area(text("close").size(12))
|
||||||
|
.on_press(Message::ClosePopup(popup_id)),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(1)
|
||||||
.padding(8)
|
|
||||||
.align_y(Alignment::Center)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generic clickable item inside a popup.
|
||||||
|
fn popup_item(label: impl Into<String>, popup_id: window::Id) -> Element<'static, Message> {
|
||||||
|
mouse_area(text(label.into()).size(13))
|
||||||
|
.on_press(Message::ClosePopup(popup_id))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Style
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
||||||
use iced::theme::Style;
|
use iced::theme::Style;
|
||||||
Style {
|
Style {
|
||||||
|
|||||||
Reference in New Issue
Block a user