pub mod blit;
pub mod clock;
pub mod config;
pub mod render;
pub mod scale;
pub mod widget;
pub mod backlight;
pub mod bluetooth;
pub mod command;
pub mod hyprland;
pub mod networkmanager;
pub mod notifications;
pub mod outputs;
pub mod power_profiles;
pub mod producer;
pub mod sni;
pub mod sysmon;
pub mod updates;
pub mod upower;
pub mod volume;
use std::error::Error;
use std::time::Duration;
use crate::blit::write_argb8888;
use crate::clock::millis_until_next_minute;
use crate::config::{Config, WidgetKind};
use crate::render::{Bounds, RenderContext, RenderSettings};
use crate::scale::Scale;
use crate::widget::{
ClickButton, Command, Dashboard, Msg, ScrollDirection, Tooltip, TrayMenu, TrayMenuItem,
TrayMenuToggleKind, TrayMenuToggleState,
};
use calloop::EventLoop;
use calloop::channel::Event as ChannelEvent;
use calloop::timer::{TimeoutAction, Timer};
use calloop_wayland_source::WaylandSource;
use log::{error, info, warn};
use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_positioner;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry,
delegate_seat, delegate_shm, delegate_xdg_popup, delegate_xdg_shell,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{
Capability, SeatHandler, SeatState,
pointer::{
BTN_LEFT, BTN_RIGHT, CursorIcon, PointerEvent, PointerEventKind, PointerHandler,
ThemeSpec, ThemedPointer,
},
},
shell::{
WaylandSurface,
wlr_layer::{
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
LayerSurfaceConfigure,
},
xdg::{
XdgPositioner, XdgShell,
popup::{Popup, PopupConfigure, PopupHandler},
window::{Window, WindowConfigure, WindowHandler},
},
},
shm::{Shm, ShmHandler, slot::SlotPool},
};
use crate::backlight::BacklightProducer;
use crate::bluetooth::BluetoothProducer;
use crate::command::{CommandSender, command_channel};
use crate::hyprland::HyprlandProducer;
use crate::networkmanager::NetworkProducer;
use crate::notifications::NotificationsProducer;
use crate::outputs::{OutputId, Outputs};
use crate::power_profiles::PowerProfilesProducer;
use crate::producer::{Producer, ProducerBridge};
use crate::sni::SniHostProducer;
use crate::sysmon::SystemProducer;
use crate::updates::UpdatesProducer;
use crate::upower::UPowerProducer;
use crate::volume::VolumeProducer;
use wayland_client::{
Connection, Dispatch, Proxy, QueueHandle,
globals::registry_queue_init,
protocol::{wl_output, wl_pointer, wl_region, wl_seat, wl_shm, wl_surface},
};
const NAMESPACE: &str = "tablero";
const INITIAL_WIDTH: u32 = 1920;
struct Surface {
output_id: OutputId,
output: wl_output::WlOutput,
layer: LayerSurface,
width: u32,
height: u32,
scale: Scale,
config: Config,
dashboard: Dashboard,
ctx: RenderContext,
configured: bool,
}
impl Surface {
fn new(
compositor: &CompositorState,
layer_shell: &LayerShell,
qh: &QueueHandle<App>,
output: &wl_output::WlOutput,
output_id: OutputId,
monitor: Option<&str>,
config: Config,
) -> Self {
let height = config.height;
let exclusive_zone = height as i32;
let wl_surface = compositor.create_surface(qh);
let layer = layer_shell.create_layer_surface(
qh,
wl_surface,
Layer::Top,
Some(NAMESPACE.to_string()),
Some(output),
);
layer.set_anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT);
layer.set_keyboard_interactivity(KeyboardInteractivity::None);
layer.set_size(0, height);
layer.set_exclusive_zone(exclusive_zone);
layer.commit();
let full = Bounds::new(0, 0, INITIAL_WIDTH, height);
let dashboard = config.build_dashboard(full, monitor);
let ctx = RenderContext::with_settings(INITIAL_WIDTH, height, config.render_settings());
Self {
output_id,
output: output.clone(),
layer,
width: INITIAL_WIDTH,
height,
scale: Scale::ONE,
config,
dashboard,
ctx,
configured: false,
}
}
fn owns(&self, wl_surface: &wl_surface::WlSurface) -> bool {
self.layer.wl_surface() == wl_surface
}
fn is_layer(&self, layer: &LayerSurface) -> bool {
self.layer.wl_surface() == layer.wl_surface()
}
fn handle(&mut self, msg: &Msg, pool: &mut SlotPool) -> bool {
let changed = self.dashboard.update(msg);
if changed {
self.draw(pool);
}
changed
}
fn set_scale(&mut self, scale: Scale, pool: &mut SlotPool) -> bool {
if self.scale == scale {
return false;
}
self.scale = scale;
self.ctx
.set_settings(self.config.scaled_render_settings(scale));
info!("output scale changed to {}x", scale.get());
self.draw(pool);
true
}
fn on_click(&self, x: f64, y: f64, button: ClickButton) -> Option<Command> {
if x < 0.0 || y < 0.0 {
return None;
}
let s = self.scale.get() as f64;
self.dashboard
.on_click((x * s) as u32, (y * s) as u32, button)
}
fn is_clickable_at(&self, x: f64, y: f64) -> bool {
if x < 0.0 || y < 0.0 {
return false;
}
let scale = self.scale.get() as f64;
self.dashboard
.is_clickable_at((x * scale) as u32, (y * scale) as u32)
}
fn on_scroll(&self, x: f64, y: f64, direction: ScrollDirection) -> Option<Command> {
if x < 0.0 || y < 0.0 {
return None;
}
let scale = self.scale.get() as f64;
self.dashboard
.on_scroll((x * scale) as u32, (y * scale) as u32, direction)
}
fn tooltip_at(&self, x: f64, y: f64) -> Option<Tooltip> {
if x < 0.0 || y < 0.0 {
return None;
}
let scale = self.scale.get() as f64;
self.dashboard
.tooltip_at((x * scale) as u32, (y * scale) as u32)
}
fn configure(&mut self, configure: LayerSurfaceConfigure, pool: &mut SlotPool) {
if configure.new_size.0 != 0 {
self.width = configure.new_size.0;
}
if configure.new_size.1 != 0 {
self.height = configure.new_size.1;
}
let first = !self.configured;
self.configured = true;
if first {
self.dashboard.update(&Msg::tick_now());
self.draw(pool);
}
}
fn draw(&mut self, pool: &mut SlotPool) {
if !self.configured {
return;
}
let (width, height) = self.scale.to_physical_size(self.width, self.height);
let stride = width as i32 * 4;
let (buffer, canvas) = match pool.create_buffer(
width as i32,
height as i32,
stride,
wl_shm::Format::Argb8888,
) {
Ok(parts) => parts,
Err(e) => {
error!("failed to create shm buffer: {e}");
return;
}
};
self.ctx.resize(width, height);
self.dashboard.layout(&mut self.ctx, width, height);
self.dashboard.draw(&mut self.ctx);
write_argb8888(self.ctx.pixels(), canvas);
let surface = self.layer.wl_surface();
surface.set_buffer_scale(self.scale.get() as i32);
surface.damage_buffer(0, 0, width as i32, height as i32);
if let Err(e) = buffer.attach_to(surface) {
error!("failed to attach buffer: {e}");
return;
}
self.layer.commit();
}
}
struct TooltipSurface {
popup: Popup,
output_id: OutputId,
text: String,
width: u32,
height: u32,
scale: Scale,
background: (u8, u8, u8, u8),
foreground: (u8, u8, u8, u8),
ctx: RenderContext,
configured: bool,
}
const MENU_ROW_HEIGHT: u32 = 28;
const MENU_SEPARATOR_HEIGHT: u32 = 8;
const POPUP_RADIUS: f32 = 6.0;
const POPUP_PADDING_X: u32 = 10;
const POPUP_PADDING_Y: u32 = 5;
const POPUP_FALLBACK_BACKGROUND: (u8, u8, u8, u8) = (0x20, 0x22, 0x27, 0xF8);
#[derive(Clone)]
struct MenuRow {
id: i32,
depth: u32,
label: String,
enabled: bool,
separator: bool,
toggle: Option<(TrayMenuToggleKind, TrayMenuToggleState)>,
has_children: bool,
}
impl MenuRow {
fn height(&self) -> u32 {
if self.separator {
MENU_SEPARATOR_HEIGHT
} else {
MENU_ROW_HEIGHT
}
}
fn activatable(&self) -> bool {
self.enabled && !self.separator && !self.has_children
}
}
fn flatten_menu(items: &[TrayMenuItem], depth: u32, rows: &mut Vec<MenuRow>) {
for item in items.iter().filter(|item| item.visible) {
rows.push(MenuRow {
id: item.id,
depth,
label: item.label.clone(),
enabled: item.enabled,
separator: item.separator,
toggle: item.toggle.map(|toggle| (toggle.kind, toggle.state)),
has_children: !item.children.iter().all(|child| !child.visible),
});
flatten_menu(&item.children, depth + 1, rows);
}
}
struct PendingTrayMenu {
key: String,
parent: LayerSurface,
output_id: OutputId,
anchor: (i32, i32),
scale: Scale,
settings: RenderSettings,
serial: u32,
seat: Option<wl_seat::WlSeat>,
}
struct TrayMenuSurface {
popup: Popup,
output_id: OutputId,
key: String,
revision: u32,
rows: Vec<MenuRow>,
width: u32,
height: u32,
scale: Scale,
background: (u8, u8, u8, u8),
foreground: (u8, u8, u8, u8),
accent: (u8, u8, u8, u8),
ctx: RenderContext,
configured: bool,
}
impl TrayMenuSurface {
fn owns(&self, popup: &Popup) -> bool {
self.popup == *popup
}
fn owns_surface(&self, surface: &wl_surface::WlSurface) -> bool {
self.popup.wl_surface() == surface
}
fn row_at(&self, y: f64) -> Option<&MenuRow> {
if y < 0.0 {
return None;
}
let mut top = 0u32;
for row in &self.rows {
let bottom = top + row.height();
if (y as u32) < bottom {
return Some(row);
}
top = bottom;
}
None
}
fn command_at(&self, y: f64) -> Option<Command> {
let row = self.row_at(y)?;
row.activatable().then(|| Command::ActivateTrayMenuItem {
key: self.key.clone(),
id: row.id,
})
}
fn update(&mut self, menu: &TrayMenu, pool: &mut SlotPool) -> bool {
if menu.revision < self.revision {
return true;
}
let mut rows = Vec::new();
flatten_menu(&menu.items, 0, &mut rows);
let height: u32 = rows.iter().map(MenuRow::height).sum();
if height != self.height {
return false;
}
self.revision = menu.revision;
self.rows = rows;
self.draw(pool);
true
}
fn draw(&mut self, pool: &mut SlotPool) {
if !self.configured {
return;
}
let scale = self.scale.get();
let width = self.width * scale;
let height = self.height * scale;
let stride = width as i32 * 4;
let (buffer, canvas) = match pool.create_buffer(
width as i32,
height as i32,
stride,
wl_shm::Format::Argb8888,
) {
Ok(parts) => parts,
Err(error) => {
warn!("failed to create tray menu buffer: {error}");
return;
}
};
self.ctx.resize(width, height);
self.ctx.fill_rounded_rect(
Bounds::new(0, 0, width, height),
self.background,
POPUP_RADIUS * scale as f32,
);
let mut top = 0u32;
for row in &self.rows {
let row_height = row.height() * scale;
if row.separator {
self.ctx.fill_rounded_rect(
Bounds::new(
POPUP_PADDING_X * scale,
top + row_height / 2,
width - 2 * POPUP_PADDING_X * scale,
scale,
),
dim_color(self.foreground),
0.0,
);
top += row_height;
continue;
}
let prefix = match row.toggle {
Some((TrayMenuToggleKind::Checkmark, TrayMenuToggleState::On)) => "[x] ",
Some((TrayMenuToggleKind::Checkmark, _)) => "[ ] ",
Some((TrayMenuToggleKind::Radio, TrayMenuToggleState::On)) => "(o) ",
Some((TrayMenuToggleKind::Radio, _)) => "( ) ",
None => "",
};
let suffix = if row.has_children { " >" } else { "" };
let label = format!("{prefix}{}{suffix}", row.label);
let indent = (POPUP_PADDING_X + row.depth * 16) * scale;
self.ctx.draw_text(
&label,
Bounds::new(
indent,
top,
width.saturating_sub(indent + POPUP_PADDING_X * scale),
row_height,
),
if row.enabled {
if row
.toggle
.is_some_and(|(_, state)| state == TrayMenuToggleState::On)
{
self.accent
} else {
self.foreground
}
} else {
dim_color(self.foreground)
},
);
top += row_height;
}
write_argb8888(self.ctx.pixels(), canvas);
self.popup
.wl_surface()
.set_buffer_scale(self.scale.get() as i32);
self.popup
.wl_surface()
.damage_buffer(0, 0, width as i32, height as i32);
if let Err(error) = buffer.attach_to(self.popup.wl_surface()) {
warn!("failed to attach tray menu buffer: {error}");
return;
}
self.popup.wl_surface().commit();
}
}
fn dim_color((r, g, b, a): (u8, u8, u8, u8)) -> (u8, u8, u8, u8) {
(r / 2, g / 2, b / 2, a)
}
fn popup_background((r, g, b, a): (u8, u8, u8, u8)) -> (u8, u8, u8, u8) {
if a < 0xC0 {
POPUP_FALLBACK_BACKGROUND
} else {
(r, g, b, a.max(0xF0))
}
}
impl TooltipSurface {
fn owns(&self, popup: &Popup) -> bool {
self.popup == *popup
}
fn owns_surface(&self, surface: &wl_surface::WlSurface) -> bool {
self.popup.wl_surface() == surface
}
fn draw(&mut self, pool: &mut SlotPool) {
if !self.configured {
return;
}
let scale = self.scale.get();
let width = self.width * scale;
let height = self.height * scale;
let stride = width as i32 * 4;
let (buffer, canvas) = match pool.create_buffer(
width as i32,
height as i32,
stride,
wl_shm::Format::Argb8888,
) {
Ok(parts) => parts,
Err(error) => {
warn!("failed to create tooltip buffer: {error}");
return;
}
};
self.ctx.resize(width, height);
self.ctx.fill_rounded_rect(
Bounds::new(0, 0, width, height),
self.background,
POPUP_RADIUS * scale as f32,
);
let padding_x = POPUP_PADDING_X * scale;
let padding_y = POPUP_PADDING_Y * scale;
let line_height = tooltip_line_height(&self.ctx);
for (index, line) in self.text.lines().enumerate() {
self.ctx.draw_text(
line,
Bounds::new(
padding_x,
padding_y + index as u32 * line_height,
width.saturating_sub(2 * padding_x),
line_height,
),
self.foreground,
);
}
write_argb8888(self.ctx.pixels(), canvas);
self.popup
.wl_surface()
.set_buffer_scale(self.scale.get() as i32);
self.popup
.wl_surface()
.damage_buffer(0, 0, width as i32, height as i32);
if let Err(error) = buffer.attach_to(self.popup.wl_surface()) {
warn!("failed to attach tooltip buffer: {error}");
return;
}
self.popup.wl_surface().commit();
}
}
fn tooltip_line_height(ctx: &RenderContext) -> u32 {
(ctx.settings().font_size * 1.15).ceil() as u32
}
fn tooltip_size(ctx: &mut RenderContext, text: &str) -> (u32, u32) {
let scale = ctx.scale_factor();
let width = text
.lines()
.map(|line| ctx.measure_text(line))
.max()
.unwrap_or(0)
+ 2 * POPUP_PADDING_X * scale;
let lines = text.lines().count().max(1) as u32;
let height = lines * tooltip_line_height(ctx) + 2 * POPUP_PADDING_Y * scale;
(width.max(1), height.max(1))
}
struct App {
registry_state: RegistryState,
output_state: OutputState,
seat_state: SeatState,
shm: Shm,
pool: SlotPool,
compositor: CompositorState,
layer_shell: LayerShell,
xdg_shell: XdgShell,
pointer: Option<ThemedPointer>,
pointer_seat: Option<wl_seat::WlSeat>,
pointer_cursor: CursorIcon,
scroll_remainder: f64,
commands: Vec<CommandSender>,
outputs: Outputs<Surface>,
tooltip: Option<TooltipSurface>,
pending_tray_menu: Option<PendingTrayMenu>,
tray_menu: Option<TrayMenuSurface>,
exit: bool,
}
impl App {
fn add_output(&mut self, output: wl_output::WlOutput, qh: &QueueHandle<App>) {
let id = output_key(&output);
let name = self.output_state.info(&output).and_then(|info| info.name);
let compositor = &self.compositor;
let layer_shell = &self.layer_shell;
let built = self.outputs.ensure(id, name.as_deref(), |config| {
Surface::new(
compositor,
layer_shell,
qh,
&output,
id,
name.as_deref(),
config,
)
});
if built {
info!(
"output {id} ({}) added; {} bar(s) live",
name.as_deref().unwrap_or("<unnamed>"),
self.outputs.len()
);
}
}
fn remove_output(&mut self, output: &wl_output::WlOutput) {
let id = output_key(output);
if self
.tray_menu
.as_ref()
.is_some_and(|menu| menu.output_id == id)
{
self.hide_tray_menu();
}
if self.outputs.remove(id).is_some() {
info!("output {id} removed; {} bar(s) live", self.outputs.len());
}
}
fn handle_all(&mut self, msg: &Msg) {
let App { pool, outputs, .. } = self;
let mut changed = false;
for surface in outputs.values_mut() {
changed |= surface.handle(msg, pool);
}
if changed && matches!(msg, Msg::PowerProfiles(_)) {
self.hide_tooltip();
}
}
fn handle_message(&mut self, msg: &Msg, qh: &QueueHandle<App>) {
if let Msg::TrayMenu(menu) = msg {
if let Some(shown) = self
.tray_menu
.as_mut()
.filter(|shown| shown.key == menu.key)
{
if !shown.update(menu, &mut self.pool) {
self.tray_menu = None;
}
} else {
self.show_tray_menu(menu, qh);
}
} else if let Msg::TrayMenuUnavailable(key) = msg {
if self
.pending_tray_menu
.as_ref()
.is_some_and(|pending| pending.key == *key)
{
self.pending_tray_menu = None;
}
} else {
self.handle_all(msg);
}
}
fn hide_tooltip(&mut self) {
self.tooltip = None;
}
fn hide_tray_menu(&mut self) {
self.pending_tray_menu = None;
self.tray_menu = None;
}
fn set_pointer_cursor(&mut self, conn: &Connection, icon: CursorIcon, force: bool) {
if !force && self.pointer_cursor == icon {
return;
}
let Some(pointer) = &self.pointer else {
return;
};
match pointer.set_cursor(conn, icon) {
Ok(()) => self.pointer_cursor = icon,
Err(error) => warn!("failed to set pointer cursor: {error}"),
}
}
fn update_tooltip(
&mut self,
surface: &wl_surface::WlSurface,
x: f64,
y: f64,
qh: &QueueHandle<App>,
) {
let request = self
.outputs
.values()
.find(|bar| bar.owns(surface))
.and_then(|bar| {
let tooltip = bar.tooltip_at(x, y)?;
Some((
bar.output_id,
bar.layer.clone(),
bar.scale,
bar.config.scaled_render_settings(bar.scale),
tooltip,
))
});
let Some((output_id, parent, scale, mut settings, tooltip)) = request else {
self.hide_tooltip();
return;
};
if self
.tooltip
.as_ref()
.is_some_and(|shown| shown.output_id == output_id && shown.text == tooltip.text)
{
return;
}
let background = popup_background(settings.background);
let foreground = settings.foreground;
settings.background = (0, 0, 0, 0);
let mut ctx = RenderContext::with_settings(1, 1, settings);
let (physical_width, physical_height) = tooltip_size(&mut ctx, &tooltip.text);
let divisor = scale.get();
let width = physical_width.div_ceil(divisor);
let height = physical_height.div_ceil(divisor);
let anchor = Bounds::new(
tooltip.bounds.x / divisor,
tooltip.bounds.y / divisor,
tooltip.bounds.width.div_ceil(divisor),
tooltip.bounds.height.div_ceil(divisor),
);
let positioner = match XdgPositioner::new(&self.xdg_shell) {
Ok(positioner) => positioner,
Err(error) => {
warn!("failed to create tooltip positioner: {error}");
return;
}
};
positioner.set_size(width as i32, height as i32);
positioner.set_anchor_rect(
anchor.x as i32,
anchor.y as i32,
anchor.width.max(1) as i32,
anchor.height.max(1) as i32,
);
positioner.set_anchor(xdg_positioner::Anchor::Bottom);
positioner.set_gravity(xdg_positioner::Gravity::Bottom);
positioner.set_constraint_adjustment(xdg_positioner::ConstraintAdjustment::SlideX);
let popup_surface = self.compositor.create_surface(qh);
let popup = match Popup::from_surface(None, &positioner, qh, popup_surface, &self.xdg_shell)
{
Ok(popup) => popup,
Err(error) => {
warn!("failed to create tooltip popup: {error}");
return;
}
};
parent.get_popup(popup.xdg_popup());
let input_region = self.compositor.wl_compositor().create_region(qh, ());
popup.wl_surface().set_input_region(Some(&input_region));
input_region.destroy();
popup.wl_surface().commit();
self.tooltip = Some(TooltipSurface {
popup,
output_id,
text: tooltip.text,
width,
height,
scale,
background,
foreground,
ctx,
configured: false,
});
}
fn show_tray_menu(&mut self, menu: &TrayMenu, qh: &QueueHandle<App>) {
let Some(pending) = self
.pending_tray_menu
.take()
.filter(|pending| pending.key == menu.key)
else {
return;
};
let mut rows = Vec::new();
flatten_menu(&menu.items, 0, &mut rows);
if rows.is_empty() {
return;
}
let mut settings = pending.settings;
let background = popup_background(settings.background);
let foreground = settings.foreground;
let accent = settings.accent;
settings.background = (0, 0, 0, 0);
let mut ctx = RenderContext::with_settings(1, 1, settings);
let scale = pending.scale.get();
let physical_width = rows
.iter()
.filter(|row| !row.separator)
.map(|row| {
let indicators = if row.toggle.is_some() { 4 } else { 0 };
let submenu = if row.has_children { 3 } else { 0 };
let text = format!(
"{}{}{}",
" ".repeat(indicators),
row.label,
" ".repeat(submenu)
);
ctx.measure_text(&text) + (32 + row.depth * 16) * scale
})
.max()
.unwrap_or(1);
let width = physical_width.div_ceil(scale).clamp(120, 420);
let height: u32 = rows.iter().map(MenuRow::height).sum();
let positioner = match XdgPositioner::new(&self.xdg_shell) {
Ok(positioner) => positioner,
Err(error) => {
warn!("failed to create tray menu positioner: {error}");
return;
}
};
positioner.set_size(width as i32, height as i32);
positioner.set_anchor_rect(pending.anchor.0, pending.anchor.1, 1, 1);
positioner.set_anchor(xdg_positioner::Anchor::BottomLeft);
positioner.set_gravity(xdg_positioner::Gravity::BottomRight);
positioner.set_constraint_adjustment(xdg_positioner::ConstraintAdjustment::SlideX);
let popup_surface = self.compositor.create_surface(qh);
let popup = match Popup::from_surface(None, &positioner, qh, popup_surface, &self.xdg_shell)
{
Ok(popup) => popup,
Err(error) => {
warn!("failed to create tray menu popup: {error}");
return;
}
};
pending.parent.get_popup(popup.xdg_popup());
if let Some(seat) = &pending.seat {
popup.xdg_popup().grab(seat, pending.serial);
}
popup.wl_surface().commit();
self.tooltip = None;
self.tray_menu = Some(TrayMenuSurface {
popup,
output_id: pending.output_id,
key: menu.key.clone(),
revision: menu.revision,
rows,
width,
height,
scale: pending.scale,
background,
foreground,
accent,
ctx,
configured: false,
});
}
}
fn output_key(output: &wl_output::WlOutput) -> OutputId {
output.id().protocol_id()
}
fn set_tray_command_position(command: &mut Command, origin: (i32, i32), local: (f64, f64)) {
let screen = (
origin.0.saturating_add(local.0 as i32),
origin.1.saturating_add(local.1 as i32),
);
match command {
Command::ActivateTrayItem { x, y, .. } | Command::OpenTrayMenu { x, y, .. } => {
*x = screen.0;
*y = screen.1;
}
_ => {}
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let mut producers: Vec<Box<dyn Producer>> = vec![
Box::new(HyprlandProducer::new()),
Box::new(UPowerProducer::new()),
Box::new(BacklightProducer::new()),
Box::new(SystemProducer::new()),
Box::new(NetworkProducer::new()),
Box::new(BluetoothProducer::new()),
Box::new(VolumeProducer::new()),
Box::new(SniHostProducer::new()),
Box::new(NotificationsProducer::new()),
Box::new(PowerProfilesProducer::new()),
];
if config.uses_widget(WidgetKind::Updates) {
producers.push(Box::new(UpdatesProducer::new()));
}
run_with_producers(config, producers)
}
pub fn run_with_producers(
config: Config,
producers: Vec<Box<dyn Producer>>,
) -> Result<(), Box<dyn Error>> {
let height = config.height;
let conn = Connection::connect_to_env()?;
let (globals, event_queue) = registry_queue_init::<App>(&conn)?;
let qh = event_queue.handle();
let compositor = CompositorState::bind(&globals, &qh)?;
let layer_shell = LayerShell::bind(&globals, &qh)?;
let xdg_shell = XdgShell::bind(&globals, &qh)?;
let shm = Shm::bind(&globals, &qh)?;
let pool = SlotPool::new((INITIAL_WIDTH * height * 4) as usize, &shm)?;
let mut app = App {
registry_state: RegistryState::new(&globals),
output_state: OutputState::new(&globals, &qh),
seat_state: SeatState::new(&globals, &qh),
shm,
pool,
compositor,
layer_shell,
xdg_shell,
pointer: None,
pointer_seat: None,
pointer_cursor: CursorIcon::Default,
scroll_remainder: 0.0,
commands: Vec::new(),
outputs: Outputs::new(config),
tooltip: None,
pending_tray_menu: None,
tray_menu: None,
exit: false,
};
let mut event_loop: EventLoop<App> = EventLoop::try_new()?;
let handle = event_loop.handle();
WaylandSource::new(conn, event_queue).insert(handle.clone())?;
let timer = Timer::from_duration(Duration::from_millis(millis_until_next_minute()));
handle.insert_source(timer, |_deadline, _, app| {
app.handle_all(&Msg::tick_now());
TimeoutAction::ToDuration(Duration::from_millis(millis_until_next_minute()))
})?;
let _bridge = if producers.is_empty() {
None
} else {
let (bridge, channel) = ProducerBridge::new()?;
let message_qh = qh.clone();
handle.insert_source(channel, move |event, _, app| {
if let ChannelEvent::Msg(msg) = event {
app.handle_message(&msg, &message_qh);
}
})?;
let count = producers.len();
for producer in producers {
bridge.spawn(producer);
}
let (hypr_tx, hypr_rx) = command_channel();
bridge.spawn_task("hyprland-commands", hyprland::run_commands(hypr_rx));
let (sni_tx, sni_rx) = command_channel();
let sni_updates = bridge.sender();
bridge.spawn_task("sni-commands", sni::run_commands(sni_rx, sni_updates));
let (run_tx, run_rx) = command_channel();
bridge.spawn_task("run-commands", command::run_commands(run_rx));
let (notif_tx, notif_rx) = command_channel();
bridge.spawn_task(
"notifications-commands",
notifications::run_commands(notif_rx),
);
let (backlight_tx, backlight_rx) = command_channel();
let backlight_updates = bridge.sender();
bridge.spawn_task(
"backlight-commands",
backlight::run_commands(backlight_rx, backlight_updates),
);
let (power_tx, power_rx) = command_channel();
bridge.spawn_task(
"power-profiles-commands",
power_profiles::run_commands(power_rx),
);
app.commands = vec![hypr_tx, sni_tx, run_tx, notif_tx, backlight_tx, power_tx];
info!("producer bridge started with {count} producer(s)");
Some(bridge)
};
info!("tablero started: one {height}px bar per output");
let signal = event_loop.get_signal();
event_loop.run(None, &mut app, move |app| {
if app.exit {
info!("all surfaces closed; shutting down");
signal.stop();
}
})?;
Ok(())
}
impl CompositorHandler for App {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
surface: &wl_surface::WlSurface,
new_factor: i32,
) {
let App {
pool,
outputs,
tooltip,
tray_menu,
..
} = self;
let scale = Scale::new(new_factor);
let changed_output = outputs
.values_mut()
.find(|bar| bar.owns(surface))
.and_then(|bar| bar.set_scale(scale, pool).then_some(bar.output_id));
if tooltip.as_ref().is_some_and(|shown| {
(shown.owns_surface(surface) && shown.scale != scale)
|| changed_output == Some(shown.output_id)
}) {
*tooltip = None;
}
if tray_menu.as_ref().is_some_and(|shown| {
(shown.owns_surface(surface) && shown.scale != scale)
|| changed_output == Some(shown.output_id)
}) {
*tray_menu = None;
}
}
fn transform_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_transform: wl_output::Transform,
) {
}
fn frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_time: u32,
) {
}
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
}
impl OutputHandler for App {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
self.add_output(output, qh);
}
fn update_output(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
self.add_output(output, qh);
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
self.remove_output(&output);
}
}
impl LayerShellHandler for App {
fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, layer: &LayerSurface) {
if let Some(id) = self
.outputs
.values()
.find(|bar| bar.is_layer(layer))
.map(|bar| bar.output_id)
{
self.outputs.remove(id);
}
if self.outputs.is_empty() {
self.exit = true;
}
}
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
layer: &LayerSurface,
configure: LayerSurfaceConfigure,
_serial: u32,
) {
let App { pool, outputs, .. } = self;
if let Some(bar) = outputs.values_mut().find(|bar| bar.is_layer(layer)) {
bar.configure(configure, pool);
}
}
}
impl PopupHandler for App {
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
popup: &Popup,
_config: PopupConfigure,
) {
if let Some(tooltip) = self.tooltip.as_mut().filter(|tooltip| tooltip.owns(popup)) {
tooltip.configured = true;
tooltip.draw(&mut self.pool);
} else if let Some(menu) = self.tray_menu.as_mut().filter(|menu| menu.owns(popup)) {
menu.configured = true;
menu.draw(&mut self.pool);
}
}
fn done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, popup: &Popup) {
if self
.tooltip
.as_ref()
.is_some_and(|tooltip| tooltip.owns(popup))
{
self.hide_tooltip();
} else if self.tray_menu.as_ref().is_some_and(|menu| menu.owns(popup)) {
self.hide_tray_menu();
}
}
}
impl WindowHandler for App {
fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &Window) {}
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_window: &Window,
_configure: WindowConfigure,
_serial: u32,
) {
}
}
impl ShmHandler for App {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
impl SeatHandler for App {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Pointer && self.pointer.is_none() {
let cursor_surface = self.compositor.create_surface(qh);
match self.seat_state.get_pointer_with_theme(
qh,
&seat,
self.shm.wl_shm(),
cursor_surface,
ThemeSpec::default(),
) {
Ok(pointer) => {
self.pointer = Some(pointer);
self.pointer_seat = Some(seat);
}
Err(e) => error!("failed to create pointer: {e}"),
}
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Pointer {
self.pointer = None;
self.pointer_seat = None;
self.pointer_cursor = CursorIcon::Default;
}
}
fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {
}
}
impl PointerHandler for App {
fn pointer_frame(
&mut self,
conn: &Connection,
_qh: &QueueHandle<Self>,
_pointer: &wl_pointer::WlPointer,
events: &[PointerEvent],
) {
for event in events {
if matches!(
event.kind,
PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. }
) {
let (x, y) = event.position;
if let Some(menu) = self
.tray_menu
.as_ref()
.filter(|menu| menu.owns_surface(&event.surface))
{
self.set_pointer_cursor(
conn,
if menu.row_at(y).is_some_and(MenuRow::activatable) {
CursorIcon::Pointer
} else {
CursorIcon::Default
},
matches!(event.kind, PointerEventKind::Enter { .. }),
);
continue;
}
let clickable = self
.outputs
.values()
.find(|bar| bar.owns(&event.surface))
.is_some_and(|bar| bar.is_clickable_at(x, y));
if self.outputs.values().any(|bar| bar.owns(&event.surface)) {
self.update_tooltip(&event.surface, x, y, _qh);
self.set_pointer_cursor(
conn,
if clickable {
CursorIcon::Pointer
} else {
CursorIcon::Default
},
matches!(event.kind, PointerEventKind::Enter { .. }),
);
}
} else if matches!(event.kind, PointerEventKind::Leave { .. }) {
if self.outputs.values().any(|bar| bar.owns(&event.surface)) {
self.hide_tooltip();
self.pointer_cursor = CursorIcon::Default;
} else if self
.tray_menu
.as_ref()
.is_some_and(|menu| menu.owns_surface(&event.surface))
{
self.pointer_cursor = CursorIcon::Default;
}
} else if let PointerEventKind::Press { button, serial, .. } = event.kind {
let click = match button {
BTN_LEFT => ClickButton::Left,
BTN_RIGHT => ClickButton::Right,
_ => continue,
};
let (x, y) = event.position;
if self
.tray_menu
.as_ref()
.is_some_and(|menu| menu.owns_surface(&event.surface))
{
if click == ClickButton::Left {
let command = self.tray_menu.as_ref().and_then(|menu| menu.command_at(y));
if let Some(command) = command {
self.hide_tray_menu();
for sender in &self.commands {
if sender.send(command.clone()).is_err() {
warn!("command channel closed; dropping menu command");
}
}
}
}
continue;
}
let interaction = self
.outputs
.values()
.find(|bar| bar.owns(&event.surface))
.and_then(|bar| {
Some((
bar.on_click(x, y, click)?,
bar.layer.clone(),
bar.output.clone(),
bar.output_id,
bar.scale,
bar.height,
bar.config.scaled_render_settings(bar.scale),
))
});
if let Some((mut command, parent, output, output_id, scale, bar_height, settings)) =
interaction
{
let origin = self
.output_state
.info(&output)
.map(|info| info.logical_position.unwrap_or(info.location))
.unwrap_or((0, 0));
set_tray_command_position(&mut command, origin, (x, y));
if let Command::OpenTrayMenu { key, .. } = &command {
self.hide_tray_menu();
self.hide_tooltip();
self.pending_tray_menu = Some(PendingTrayMenu {
key: key.clone(),
parent,
output_id,
anchor: (x as i32, bar_height as i32),
scale,
settings,
serial,
seat: self.pointer_seat.clone(),
});
}
for sender in &self.commands {
if sender.send(command.clone()).is_err() {
warn!("command channel closed; dropping click command");
}
}
}
} else if let PointerEventKind::Axis { vertical, .. } = event.kind {
let directions = scroll_directions(vertical, &mut self.scroll_remainder);
for direction in directions {
let (x, y) = event.position;
let command = self
.outputs
.values()
.find(|bar| bar.owns(&event.surface))
.and_then(|bar| bar.on_scroll(x, y, direction));
if let Some(command) = command {
for sender in &self.commands {
if sender.send(command.clone()).is_err() {
warn!("command channel closed; dropping scroll command");
}
}
}
}
}
}
}
}
impl Dispatch<wl_region::WlRegion, ()> for App {
fn event(
_state: &mut Self,
_proxy: &wl_region::WlRegion,
_event: wl_region::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
fn scroll_directions(
vertical: smithay_client_toolkit::seat::pointer::AxisScroll,
remainder: &mut f64,
) -> Vec<ScrollDirection> {
let delta = if vertical.value120 != 0 {
vertical.value120 as f64 / 120.0
} else if vertical.discrete != 0 {
vertical.discrete as f64
} else {
vertical.absolute / 10.0
};
*remainder += delta;
let mut directions = Vec::new();
while *remainder >= 1.0 {
directions.push(ScrollDirection::Decrease);
*remainder -= 1.0;
}
while *remainder <= -1.0 {
directions.push(ScrollDirection::Increase);
*remainder += 1.0;
}
directions
}
#[cfg(test)]
mod scroll_tests {
use super::*;
use smithay_client_toolkit::seat::pointer::AxisScroll;
#[test]
fn wheel_steps_map_up_to_increase_and_down_to_decrease() {
let mut remainder = 0.0;
assert_eq!(
scroll_directions(
AxisScroll {
value120: -120,
..AxisScroll::default()
},
&mut remainder,
),
vec![ScrollDirection::Increase]
);
assert_eq!(
scroll_directions(
AxisScroll {
value120: 120,
..AxisScroll::default()
},
&mut remainder,
),
vec![ScrollDirection::Decrease]
);
}
#[test]
fn smooth_motion_accumulates_before_emitting_a_step() {
let mut remainder = 0.0;
let half = AxisScroll {
absolute: -5.0,
..AxisScroll::default()
};
assert!(scroll_directions(half, &mut remainder).is_empty());
assert_eq!(
scroll_directions(half, &mut remainder),
vec![ScrollDirection::Increase]
);
}
#[test]
fn tray_coordinates_include_the_output_logical_origin() {
let mut command = Command::OpenTrayMenu {
key: ":1.7/Menu".into(),
x: 0,
y: 0,
};
set_tray_command_position(&mut command, (1920, -40), (24.8, 18.9));
assert_eq!(
command,
Command::OpenTrayMenu {
key: ":1.7/Menu".into(),
x: 1944,
y: -22,
}
);
}
#[test]
fn tray_menu_flattens_visible_nested_entries_and_preserves_depth() {
let leaf = TrayMenuItem {
id: 2,
label: "Child".into(),
enabled: true,
visible: true,
separator: false,
toggle: None,
children: vec![],
};
let parent = TrayMenuItem {
id: 1,
label: "Parent".into(),
enabled: true,
visible: true,
separator: false,
toggle: None,
children: vec![leaf],
};
let hidden = TrayMenuItem {
id: 3,
label: "Hidden".into(),
enabled: true,
visible: false,
separator: false,
toggle: None,
children: vec![],
};
let mut rows = Vec::new();
flatten_menu(&[parent, hidden], 0, &mut rows);
assert_eq!(rows.len(), 2);
assert_eq!((rows[0].id, rows[0].depth), (1, 0));
assert!(rows[0].has_children);
assert!(!rows[0].activatable());
assert_eq!((rows[1].id, rows[1].depth), (2, 1));
assert!(rows[1].activatable());
}
#[test]
fn transparent_bar_background_uses_an_opaque_popup_surface() {
assert_eq!(
popup_background((0x18, 0x18, 0x18, 0x00)),
POPUP_FALLBACK_BACKGROUND
);
assert_eq!(
popup_background((0x30, 0x32, 0x38, 0xD0)),
(0x30, 0x32, 0x38, 0xF0)
);
}
}
impl ProvidesRegistryState for App {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
delegate_compositor!(App);
delegate_output!(App);
delegate_shm!(App);
delegate_layer!(App);
delegate_xdg_shell!(App);
delegate_xdg_popup!(App);
delegate_seat!(App);
delegate_pointer!(App);
delegate_registry!(App);