Skip to content

Commit 242a98f

Browse files
feat(ui): replace global configuration drawer with a regular xdg toplevel
Signed-off-by: Maika Namuo <httpworldview@gmail.com>
1 parent 28007ae commit 242a98f

16 files changed

Lines changed: 145 additions & 269 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ Fedora, and tarball artifacts.
187187

188188
| Binding | Action |
189189
| --- | --- |
190-
| `ctrl+shift+h` | Show or hide the global configuration drawer. |
190+
| `ctrl+shift+h` | Show/hide the configuration window; while open, drag visuals to rearrange them. |
191191
| right click on a visual | Open that visual's settings window. |
192192
| `p` | Pause or resume meter updates. |
193193
| `q` twice | Quit the application. |

‎src/ui.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
// Copyright (C) 2026 Maika Namuo
33

4+
macro_rules! controls {
5+
($spacing:literal; $($control:expr;)*) => {{
6+
controls!(@push iced::widget::Column::new().spacing($spacing); $($control;)*)
7+
}};
8+
($base:expr; $($control:expr;)*) => {{
9+
controls!(@push $base; $($control;)*)
10+
}};
11+
(@push $base:expr; $($control:expr;)*) => {{
12+
let mut column = $base;
13+
$(column = column.push($control);)*
14+
column
15+
}};
16+
}
17+
18+
macro_rules! slider {
19+
($label:expr, $value:expr, $range:expr, $on_change:expr, $fmt:literal) => {
20+
$crate::ui::settings::widgets::slide(
21+
$label,
22+
$value,
23+
format!($fmt, $value),
24+
$range,
25+
$on_change,
26+
)
27+
};
28+
($label:expr, $value:expr, $range:expr, $on_change:expr, $display:expr) => {
29+
$crate::ui::settings::widgets::slide($label, $value, $display, $range, $on_change)
30+
};
31+
}
32+
433
pub mod app;
534
pub mod config;
635
pub mod settings;

‎src/ui/app.rs‎

Lines changed: 15 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,12 @@ use std::rc::Rc;
3636
use std::sync::{Arc, mpsc};
3737
use std::time::{Duration, Instant};
3838
use windowing::{
39-
APP_ID, BarResizeState, PopoutWindow, layershell_available, main_window_size, open_main_window,
39+
APP_ID, BarResizeState, PopoutWindow, layershell_available, main_window_size,
40+
open_config_base_window, open_main_window,
4041
};
4142

4243
const TOAST_DISPLAY_DURATION: Duration = Duration::from_secs(2);
43-
const DEFAULT_DRAWER_RATIO: f32 = 0.20;
44-
const MIN_DRAWER_RATIO: f32 = 0.10;
45-
const MAX_DRAWER_RATIO: f32 = 0.50;
4644
const BAR_RESIZE_HANDLE_THICKNESS: f32 = 6.0;
47-
const DRAWER_RESIZE_HANDLE_WIDTH: f32 = 6.0;
4845

4946
#[derive(Clone)]
5047
pub(crate) struct UiConfig {
@@ -96,10 +93,7 @@ struct UiApp {
9693
visual_manager: VisualManagerHandle,
9794
settings_handle: SettingsHandle,
9895
audio_frames: Arc<AsyncReceiver<AudioBatch>>,
99-
drawer_open: bool,
100-
drawer_width_ratio: f32,
101-
drawer_resizing: bool,
102-
drawer_resize_offset: Option<f32>,
96+
config_window: Option<window::Id>,
10397
bar_resize_state: Option<BarResizeState>,
10498
rendering_paused: bool,
10599
toast_until: Option<Instant>,
@@ -156,10 +150,7 @@ impl UiApp {
156150
visual_manager,
157151
settings_handle,
158152
audio_frames,
159-
drawer_open: false,
160-
drawer_width_ratio: DEFAULT_DRAWER_RATIO,
161-
drawer_resizing: false,
162-
drawer_resize_offset: None,
153+
config_window: None,
163154
bar_resize_state: None,
164155
rendering_paused: false,
165156
toast_until: None,
@@ -194,38 +185,20 @@ impl UiApp {
194185
}),
195186
];
196187
subs.push(channel_subscription(Arc::clone(&self.audio_frames)).map(Message::AudioFrame));
197-
if self.drawer_resizing && self.drawer_open {
198-
subs.push(event::listen_with(message::drawer_drag_events));
199-
}
200188
if self.bar_resize_state.is_some() {
201189
subs.push(event::listen_with(message::bar_drag_events));
202190
}
203191
Subscription::batch(subs)
204192
}
205193

206-
fn toggle_drawer(&mut self) {
207-
self.drawer_open = !self.drawer_open;
208-
self.end_drawer_resize();
209-
self.toast_until = self
210-
.drawer_open
211-
.then(|| Instant::now() + TOAST_DISPLAY_DURATION);
212-
}
213-
214-
fn end_drawer_resize(&mut self) {
215-
self.drawer_resizing = false;
216-
self.drawer_resize_offset = None;
217-
}
218-
219-
fn handle_drawer_resize(&mut self, position: iced::Point) {
220-
if self.drawer_resizing && self.drawer_width_ratio > 0.0 {
221-
let estimated_width = self.drawer_resize_offset.get_or_insert_with(|| {
222-
(position.x - DRAWER_RESIZE_HANDLE_WIDTH) / self.drawer_width_ratio
223-
});
224-
if *estimated_width > 0.0 {
225-
self.drawer_width_ratio =
226-
(position.x / *estimated_width).clamp(MIN_DRAWER_RATIO, MAX_DRAWER_RATIO);
227-
}
194+
fn toggle_config_window(&mut self) -> Task<Message> {
195+
if let Some(id) = self.config_window.take() {
196+
return window::close(id);
228197
}
198+
let (id, task) = open_config_base_window(self.use_layershell);
199+
self.config_window = Some(id);
200+
self.toast_until = Some(Instant::now() + TOAST_DISPLAY_DURATION);
201+
task
229202
}
230203

231204
fn begin_bar_resize(&mut self) {
@@ -288,22 +261,19 @@ impl UiApp {
288261
};
289262

290263
let content = self.visuals_with_toasts();
291-
let content = self.wrap_drawer(content);
292264
let content = self.wrap_bar_resize(content, &bar);
293265
Self::wrap_window_resize(content, use_decorations, &bar)
294266
}
295267

296268
fn visuals_with_toasts(&self) -> Element<'_, Message> {
297-
let visuals_view = self
298-
.visuals_page
299-
.view(self.drawer_open)
300-
.map(Message::Visuals);
269+
let config_open = self.config_window.is_some();
270+
let visuals_view = self.visuals_page.view(config_open).map(Message::Visuals);
301271

302272
let now = Instant::now();
303273
let is_active = |deadline: Option<Instant>| deadline.is_some_and(|expires| now < expires);
304274
let toast_msgs: Vec<&str> = [
305-
(self.drawer_open && is_active(self.toast_until))
306-
.then_some("ctrl+shift+h to close drawer"),
275+
(config_open && is_active(self.toast_until))
276+
.then_some("drag visuals to rearrange | ctrl+shift+h to close config"),
307277
self.rendering_paused.then_some("paused (p to resume)"),
308278
is_active(self.exit_warning_until).then_some("q again to exit"),
309279
]
@@ -329,36 +299,6 @@ impl UiApp {
329299
layer.into()
330300
}
331301

332-
fn wrap_drawer<'a>(&'a self, visuals: Element<'a, Message>) -> Element<'a, Message> {
333-
if !self.drawer_open {
334-
return visuals;
335-
}
336-
let drawer_portion = (self.drawer_width_ratio * 1000.0).round() as u16;
337-
let visuals_portion = 1000 - drawer_portion;
338-
let drawer: Element<'_, Message> = fill!(self.config_page.view().map(Message::Config))
339-
.width(Length::FillPortion(drawer_portion))
340-
.style(theme::opaque_container)
341-
.into();
342-
let handle: Element<'_, Message> = mouse_area(
343-
container(text(":").size(12).align_x(Horizontal::Center))
344-
.width(12)
345-
.height(Length::Fill)
346-
.align_x(Horizontal::Center)
347-
.align_y(Vertical::Center)
348-
.style(theme::resize_handle_container),
349-
)
350-
.on_press(Message::DrawerResizeStart)
351-
.interaction(iced::mouse::Interaction::ResizingHorizontally)
352-
.into();
353-
let visuals: Element<'_, Message> = fill!(visuals)
354-
.width(Length::FillPortion(visuals_portion))
355-
.into();
356-
row![drawer, handle, visuals]
357-
.width(Length::Fill)
358-
.height(Length::Fill)
359-
.into()
360-
}
361-
362302
fn wrap_bar_resize<'a>(
363303
&'a self,
364304
content: Element<'a, Message>,

‎src/ui/app/message.rs‎

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,9 @@ pub(super) enum Message {
2424
Visuals(VisualsMessage),
2525
AudioFrame(AudioBatch),
2626
BarOutputResolved(window::Id, Option<OutputSnapshot>),
27-
ToggleDrawer,
27+
ToggleConfig,
2828
TogglePause,
2929
PopOutOrDock(window::Id),
30-
DrawerResizeStart,
31-
DrawerResizeMove(iced::Point),
32-
DrawerResizeEnd,
3330
BarResizeStart,
3431
BarResizeMove(iced::Point),
3532
BarResizeEnd,
@@ -63,10 +60,6 @@ fn drag_events(
6360
}
6461
}
6562

66-
pub(super) fn drawer_drag_events(evt: Event, _: event::Status, _: window::Id) -> Option<Message> {
67-
drag_events(evt, Message::DrawerResizeMove, Message::DrawerResizeEnd)
68-
}
69-
7063
pub(super) fn bar_drag_events(evt: Event, _: event::Status, _: window::Id) -> Option<Message> {
7164
drag_events(evt, Message::BarResizeMove, Message::BarResizeEnd)
7265
}
@@ -83,7 +76,7 @@ pub(super) fn keyboard_shortcut(
8376
(modifiers.control(), modifiers.shift(), modifiers.is_empty());
8477
match key {
8578
Key::Character(ch) if ctrl && shift && ch.eq_ignore_ascii_case("h") => {
86-
Some(Message::ToggleDrawer)
79+
Some(Message::ToggleConfig)
8780
}
8881
Key::Named(keyboard::key::Named::Space) if ctrl => Some(Message::PopOutOrDock(window_id)),
8982
Key::Character(ch) if no_modifiers && status != event::Status::Captured => {
@@ -130,30 +123,12 @@ pub(super) fn update(app: &mut UiApp, msg: Message) -> Task<Message> {
130123
window::drag(app.main_window_id)
131124
}
132125
Message::Visuals(visuals_msg) => app.visuals_page.update(visuals_msg).map(Message::Visuals),
133-
Message::ToggleDrawer => {
134-
app.toggle_drawer();
135-
Task::none()
136-
}
126+
Message::ToggleConfig => app.toggle_config_window(),
137127
Message::TogglePause => {
138128
app.rendering_paused = !app.rendering_paused;
139129
Task::none()
140130
}
141131
Message::PopOutOrDock(window_id) => app.handle_popout_or_dock(window_id),
142-
Message::DrawerResizeStart => {
143-
if app.drawer_open {
144-
app.drawer_resizing = true;
145-
app.drawer_resize_offset = None;
146-
}
147-
Task::none()
148-
}
149-
Message::DrawerResizeMove(pos) => {
150-
app.handle_drawer_resize(pos);
151-
Task::none()
152-
}
153-
Message::DrawerResizeEnd => {
154-
app.end_drawer_resize();
155-
Task::none()
156-
}
157132
Message::BarResizeStart => {
158133
app.begin_bar_resize();
159134
Task::none()
@@ -211,6 +186,13 @@ pub(super) fn view(app: &UiApp, window_id: window::Id) -> Element<'_, Message> {
211186
if window_id == app.main_window_id {
212187
return app.main_window_view();
213188
}
189+
if app.config_window == Some(window_id) {
190+
let content: Element<'_, Message> = fill!(app.config_page.view().map(Message::Config))
191+
.padding(16)
192+
.style(theme::weak_container)
193+
.into();
194+
return content;
195+
}
214196
if let Some((_, panel)) = app
215197
.settings_window
216198
.as_ref()

‎src/ui/app/windowing.rs‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use wayland_client::{Connection, Dispatch, QueueHandle};
2525

2626
pub(super) const APP_ID: &str = "openmeters-ui";
2727
const WINDOW_MIN_SIZE: Size = Size::new(200.0, 150.0);
28-
pub(super) const SETTINGS_WINDOW_SIZE: Size = Size::new(480.0, 600.0);
28+
const TOOL_WINDOW_SIZE: Size = Size::new(480.0, 600.0);
2929

3030
#[derive(Debug, Default)]
3131
struct LayerShellProbe;
@@ -121,8 +121,12 @@ fn open_base_window(
121121
}
122122
}
123123

124+
pub(super) fn open_config_base_window(use_layershell: bool) -> (window::Id, Task<Message>) {
125+
open_base_window(use_layershell, TOOL_WINDOW_SIZE, true)
126+
}
127+
124128
fn open_settings_base_window(use_layershell: bool) -> (window::Id, Task<Message>) {
125-
open_base_window(use_layershell, SETTINGS_WINDOW_SIZE, true)
129+
open_base_window(use_layershell, TOOL_WINDOW_SIZE, true)
126130
}
127131

128132
pub(super) fn open_main_window(
@@ -332,6 +336,9 @@ impl UiApp {
332336
if id == self.main_window_id {
333337
return exit();
334338
}
339+
if self.config_window == Some(id) {
340+
self.config_window = None;
341+
}
335342
if self.settings_window.as_ref().is_some_and(|(w, _)| *w == id) {
336343
self.settings_window = None;
337344
}
@@ -391,6 +398,10 @@ impl UiApp {
391398
return "OpenMeters".into();
392399
}
393400

401+
if self.config_window == Some(window_id) {
402+
return "Configuration - OpenMeters".into();
403+
}
404+
394405
let (kind, suffix) = if let Some((_, panel)) = self
395406
.settings_window
396407
.as_ref()
@@ -407,10 +418,12 @@ impl UiApp {
407418
}
408419

409420
pub(super) fn theme(&self, window_id: window::Id) -> iced::Theme {
421+
let is_config = self.config_window == Some(window_id);
410422
let is_settings = matches!(&self.settings_window, Some((w, _)) if *w == window_id);
411-
// Settings window forces opaque alpha: it has no wgpu visual backdrop, so a
423+
let is_tool = is_config || is_settings;
424+
// Tool windows force opaque alpha: they have no wgpu visual backdrop, so a
412425
// translucent user background would let the desktop bleed through the chrome.
413-
let custom_bg = if is_settings
426+
let custom_bg = if is_tool
414427
|| window_id == self.main_window_id
415428
|| self.popout_windows.contains_key(&window_id)
416429
{
@@ -420,7 +433,7 @@ impl UiApp {
420433
}
421434
.map(|c| {
422435
let c: iced::Color = c.into();
423-
if is_settings { with_alpha(c, 1.0) } else { c }
436+
if is_tool { with_alpha(c, 1.0) } else { c }
424437
});
425438
theme::theme(custom_bg)
426439
}

0 commit comments

Comments
 (0)