Skip to content

Commit 4e70222

Browse files
fix(ui): stabilize bar monitor hotplug recovery
Signed-off-by: Maika Namuo <httpworldview@gmail.com>
1 parent 073158c commit 4e70222

5 files changed

Lines changed: 240 additions & 263 deletions

File tree

‎src/persistence/schema.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl UiSettings {
109109
if let Some(value) = map.remove("bar") {
110110
out.bar = lossy::settings(value, "bar", BarSettings::default(), |map, out| {
111111
lossy::fields!(map, out, "bar"; enabled, alignment, height, monitor);
112+
out.monitor = out.monitor.take().filter(|name| !name.is_empty());
112113
});
113114
}
114115
lossy::fields!(map, out, "settings";
@@ -248,6 +249,8 @@ mod tests {
248249
assert_eq!(settings.bar.alignment, BarAlignment::Bottom);
249250
assert_eq!(settings.bar.height, BAR_DEFAULT_HEIGHT);
250251
assert_eq!(settings.bar.monitor.as_deref(), Some("HDMI-A-1"));
252+
let empty_monitor = UiSettings::from_json_lossy(r#"{"bar":{"monitor":""}}"#).unwrap();
253+
assert!(empty_monitor.bar.monitor.is_none());
251254

252255
assert_eq!(settings.visuals.order, vec![VisualKind::Spectrum]);
253256
assert_eq!(settings.visuals.width_basis.len(), 1);

‎src/ui/app.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use windowing::{
3636
};
3737

3838
const TOAST_DISPLAY_DURATION: Duration = Duration::from_secs(2);
39+
const BAR_RETRY_WINDOW: Duration = Duration::from_secs(5);
3940
const MAINTENANCE_INTERVAL: Duration = Duration::from_millis(100);
4041
const BAR_RESIZE_HANDLE_THICKNESS: f32 = 6.0;
4142

@@ -117,7 +118,9 @@ struct UiApp {
117118
main_window_size: Size,
118119
last_base_window_size: Size,
119120
main_window_is_layer: bool,
120-
opened_main_layer_window: Option<window::Id>,
121+
main_layer_opened: bool,
122+
main_layer_ready: bool,
123+
last_bar_retry: Option<Instant>,
121124
use_layershell: bool,
122125
settings_window: Option<(window::Id, ActiveSettings)>,
123126
settings_scroll: ScrollGlow,
@@ -189,7 +192,9 @@ impl UiApp {
189192
main_window_size: main_size,
190193
last_base_window_size: base_size,
191194
main_window_is_layer: main_is_layer,
192-
opened_main_layer_window: None,
195+
main_layer_opened: false,
196+
main_layer_ready: false,
197+
last_bar_retry: None,
193198
use_layershell,
194199
settings_window: None,
195200
settings_scroll: ScrollGlow::default(),

‎src/ui/app/message.rs‎

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

4-
use super::{TOAST_DISPLAY_DURATION, UiApp, windowing::AppWindow};
5-
use crate::ui::config::{BarChange, BarOutputEvent, ConfigEffect, ConfigMessage};
4+
use super::{BAR_RETRY_WINDOW, TOAST_DISPLAY_DURATION, UiApp, windowing::AppWindow};
5+
use crate::ui::config::{BarOutputChange, BarOutputEvent, ConfigEffect, ConfigMessage};
66
use crate::ui::settings::SettingsMessage;
77
use crate::ui::visuals::VisualsMessage;
88
use crate::ui::widgets::{fill, page, scroll_glow::ScrollGlow};
@@ -50,16 +50,12 @@ pub(super) fn layershell_open(settings: NewLayerShellSettings) -> (window::Id, T
5050
}
5151

5252
pub(super) fn shell_event(event: ShellEvent) -> Option<Message> {
53+
use BarOutputEvent as Change;
54+
5355
Some(match event {
54-
ShellEvent::OutputAdded(output) => {
55-
Message::BarOutput(output.id, output.name, BarOutputEvent::Added)
56-
}
57-
ShellEvent::OutputUpdated(output) => {
58-
Message::BarOutput(output.id, output.name, BarOutputEvent::Updated)
59-
}
60-
ShellEvent::OutputRemoved(output) => {
61-
Message::BarOutput(output.id, output.name, BarOutputEvent::Removed)
62-
}
56+
ShellEvent::OutputAdded(o) => Message::BarOutput(o.id, o.name, Change::Added),
57+
ShellEvent::OutputUpdated(o) => Message::BarOutput(o.id, o.name, Change::Updated),
58+
ShellEvent::OutputRemoved(o) => Message::BarOutput(o.id, o.name, Change::Removed),
6359
ShellEvent::WindowOutputChanged { window, output } => {
6460
Message::BarWindowOutput(window, output.map(|output| output.id))
6561
}
@@ -113,11 +109,22 @@ pub(super) fn app_event(
113109
}
114110
}
115111

112+
fn retry_bar(app: &mut UiApp, closed: bool) -> Task<Message> {
113+
if app
114+
.last_bar_retry
115+
.is_some_and(|retry| retry.elapsed() < BAR_RETRY_WINDOW)
116+
{
117+
return if closed { exit() } else { Task::none() };
118+
}
119+
app.last_bar_retry = Some(Instant::now());
120+
app.recreate_main_window(!closed)
121+
}
122+
116123
fn close_main_layer(app: &mut UiApp, window: window::Id) -> Task<Message> {
117-
if app.config_page.retry_bar_after_close() {
118-
app.handle_bar_config_change(BarChange::Monitor)
119-
} else {
124+
if app.main_layer_ready {
120125
app.on_window_closed(window)
126+
} else {
127+
retry_bar(app, true)
121128
}
122129
}
123130

@@ -141,11 +148,11 @@ pub(super) fn update(app: &mut UiApp, msg: Message) -> Task<Message> {
141148
app.frames.borrow_mut().set_rate(rate);
142149
Task::none()
143150
}
144-
Some(ConfigEffect::DecorationsChanged(enabled)) if app.main_window_is_layer => {
145-
app.recreate_popout_windows(enabled)
151+
Some(ConfigEffect::DecorationsChanged) => app.recreate_visual_windows(),
152+
Some(ConfigEffect::BarChanged(change)) => {
153+
app.last_bar_retry = None;
154+
app.handle_bar_config_change(change)
146155
}
147-
Some(ConfigEffect::DecorationsChanged(enabled)) => app.recreate_windows(enabled),
148-
Some(ConfigEffect::BarChanged(change)) => app.handle_bar_config_change(change),
149156
Some(ConfigEffect::ThemeChanged) => {
150157
if let Some((_, panel)) = app.settings_window.as_mut() {
151158
*panel = super::ActiveSettings::new(panel.kind(), &app.visual_manager);
@@ -154,9 +161,7 @@ pub(super) fn update(app: &mut UiApp, msg: Message) -> Task<Message> {
154161
}
155162
None => Task::none(),
156163
},
157-
Message::Visuals(VisualsMessage::SettingsRequested(kind)) => {
158-
app.open_settings_window(kind, false)
159-
}
164+
Message::Visuals(VisualsMessage::SettingsRequested(kind)) => app.open_settings_window(kind),
160165
Message::Visuals(visuals_msg) => app.visuals_page.update(visuals_msg).map(Message::Visuals),
161166
Message::ToggleConfig => app.toggle_config_window(),
162167
Message::TogglePause => {
@@ -189,23 +194,31 @@ pub(super) fn update(app: &mut UiApp, msg: Message) -> Task<Message> {
189194
Task::none()
190195
}
191196
Message::BarOutput(id, name, event) => {
192-
if app.config_page.sync_bar_output(id, name, event) {
193-
app.handle_bar_config_change(BarChange::Monitor)
197+
let change = app.config_page.sync_bar_output(id, name, event);
198+
if app.main_window_is_layer && change != BarOutputChange::Unchanged {
199+
app.last_bar_retry = None;
200+
}
201+
if app.main_window_is_layer && change == BarOutputChange::CurrentRemoved {
202+
app.main_layer_ready = false;
203+
}
204+
if app.main_window_is_layer && change == BarOutputChange::Retarget {
205+
retry_bar(app, false)
194206
} else {
195207
Task::none()
196208
}
197209
}
198-
Message::BarWindowOutput(window, output) => {
199-
if app.main_window_is_layer
200-
&& window == app.main_window_id
201-
&& app.config_page.sync_current_bar_output(output)
202-
{
203-
app.handle_bar_config_change(BarChange::Monitor)
210+
Message::BarWindowOutput(window, output)
211+
if app.main_window_is_layer && window == app.main_window_id =>
212+
{
213+
app.main_layer_ready = true;
214+
if app.config_page.sync_current_bar_output(output) {
215+
retry_bar(app, false)
204216
} else {
205217
Task::none()
206218
}
207219
}
208-
// Shell close events are ordered after output changes in the same stream.
220+
Message::BarWindowOutput(_, _) => Task::none(),
221+
// Output changes and shell closes share one ordered event stream.
209222
Message::ShellWindowClosed(window)
210223
if app.main_window_is_layer && window == app.main_window_id =>
211224
{
@@ -214,15 +227,15 @@ pub(super) fn update(app: &mut UiApp, msg: Message) -> Task<Message> {
214227
Message::ShellWindowClosed(_) => Task::none(),
215228
Message::WindowOpened(window) => {
216229
if app.main_window_is_layer && window == app.main_window_id {
217-
app.opened_main_layer_window = Some(window);
230+
app.main_layer_opened = true;
218231
}
219232
Task::none()
220233
}
221234
// After Opened, the ordered shell close is authoritative for the main layer.
222235
Message::WindowClosed(window)
223236
if app.main_window_is_layer && window == app.main_window_id =>
224237
{
225-
if app.opened_main_layer_window == Some(window) {
238+
if app.main_layer_opened {
226239
Task::none()
227240
} else {
228241
close_main_layer(app, window)

‎src/ui/app/windowing.rs‎

Lines changed: 45 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -186,29 +186,23 @@ impl UiApp {
186186
}
187187
}
188188

189-
pub(super) fn open_settings_window(
190-
&mut self,
191-
kind: VisualKind,
192-
force_new: bool,
193-
) -> Task<Message> {
194-
let new_panel = ActiveSettings::new(kind, &self.visual_manager);
189+
pub(super) fn open_settings_window(&mut self, kind: VisualKind) -> Task<Message> {
190+
let panel = ActiveSettings::new(kind, &self.visual_manager);
195191
let previous = self.settings_window.take();
196-
let same_kind = previous
192+
if previous
197193
.as_ref()
198-
.is_some_and(|(_, panel)| panel.kind() == kind);
199-
if same_kind && !force_new {
200-
self.settings_window = previous.map(|(id, _)| (id, new_panel));
194+
.is_some_and(|(_, current)| current.kind() == kind)
195+
{
196+
self.settings_window = previous.map(|(id, _)| (id, panel));
201197
return Task::none();
202198
}
203-
let (new_id, open_task) = open_tool_base_window(self.use_layershell);
204-
if !same_kind {
205-
self.settings_scroll = ScrollGlow::default();
206-
}
207-
self.settings_window = Some((new_id, new_panel));
199+
200+
let (id, open) = open_tool_base_window(self.use_layershell);
201+
self.settings_scroll = ScrollGlow::default();
202+
self.settings_window = Some((id, panel));
208203
match previous {
209-
Some((old_id, _)) if force_new => Task::batch([open_task, window::close(old_id)]),
210-
Some((old_id, _)) => Task::batch([window::close(old_id), open_task]),
211-
None => open_task,
204+
Some((old, _)) => Task::batch([window::close(old), open]),
205+
None => open,
212206
}
213207
}
214208

@@ -476,73 +470,65 @@ impl UiApp {
476470
Task::none()
477471
}
478472

479-
pub(super) fn recreate_main_window(
480-
&mut self,
481-
bar_settings: BarSettings,
482-
use_decorations: bool,
483-
) -> Task<Message> {
473+
pub(super) fn recreate_main_window(&mut self, close_old: bool) -> Task<Message> {
484474
let old_main_id = self.main_window_id;
475+
let (bar, decorations) = {
476+
let settings = &self.settings_handle.borrow().data;
477+
(settings.bar.clone(), settings.decorations)
478+
};
479+
self.config_page.sync_current_bar_output(None);
480+
self.main_layer_opened = false;
481+
self.main_layer_ready = false;
485482
let (new_main_id, open_main, main_is_layer, main_size) = open_main_window(
486483
self.use_layershell,
487-
bar_settings,
484+
bar,
488485
self.last_base_window_size,
489-
use_decorations,
486+
decorations,
490487
);
491488
self.main_window_id = new_main_id;
492489
self.main_window_size = main_size;
493490
self.main_window_is_layer = main_is_layer;
494-
Task::batch([open_main, window::close(old_main_id)])
491+
if close_old {
492+
Task::batch([open_main, window::close(old_main_id)])
493+
} else {
494+
open_main
495+
}
495496
}
496497

497498
pub(super) fn handle_bar_config_change(&mut self, change: BarChange) -> Task<Message> {
498499
if !self.use_layershell {
499500
return Task::none();
500501
}
501-
let (bar, decorations) = {
502-
let settings = &self.settings_handle.borrow().data;
503-
(settings.bar.clone(), settings.decorations)
504-
};
502+
let bar = self.settings_handle.borrow().data.bar.clone();
505503
match change {
506504
BarChange::Mode if bar.enabled != self.main_window_is_layer => {
507-
self.recreate_main_window(bar, decorations)
508-
}
509-
BarChange::Monitor if self.main_window_is_layer => {
510-
self.recreate_main_window(bar, decorations)
505+
self.recreate_main_window(true)
511506
}
507+
BarChange::Monitor if self.main_window_is_layer => self.recreate_main_window(true),
512508
BarChange::Mode | BarChange::Layout if self.main_window_is_layer => {
513509
self.apply_bar_layout(bar.alignment, bar.height)
514510
}
515511
BarChange::Mode | BarChange::Layout | BarChange::Monitor => Task::none(),
516512
}
517513
}
518514

519-
pub(super) fn recreate_popout_windows(&mut self, use_decorations: bool) -> Task<Message> {
515+
pub(super) fn recreate_visual_windows(&mut self) -> Task<Message> {
516+
let decorations = self.settings_handle.borrow().data.decorations;
520517
let old_popouts = std::mem::take(&mut self.popout_windows);
521-
let mut tasks = Vec::with_capacity(old_popouts.len() * 2);
522-
for (old_id, popout) in old_popouts {
523-
let (new_id, open_task) =
524-
open_base_window(self.use_layershell, popout.size, use_decorations);
525-
self.popout_windows.insert(new_id, popout);
526-
tasks.push(open_task);
527-
tasks.push(window::close(old_id));
518+
let mut tasks = Vec::with_capacity(old_popouts.len() * 2 + 2);
519+
520+
if !self.main_window_is_layer {
521+
let old = self.main_window_id;
522+
let (id, open) =
523+
open_base_window(self.use_layershell, self.main_window_size, decorations);
524+
self.main_window_id = id;
525+
tasks.extend([open, window::close(old)]);
526+
}
527+
for (old, popout) in old_popouts {
528+
let (id, open) = open_base_window(self.use_layershell, popout.size, decorations);
529+
self.popout_windows.insert(id, popout);
530+
tasks.extend([open, window::close(old)]);
528531
}
529532
Task::batch(tasks)
530533
}
531-
532-
pub(super) fn recreate_windows(&mut self, use_decorations: bool) -> Task<Message> {
533-
let old_main_id = self.main_window_id;
534-
let (new_main_id, open_main) =
535-
open_base_window(self.use_layershell, self.main_window_size, use_decorations);
536-
self.main_window_id = new_main_id;
537-
self.main_window_is_layer = false;
538-
let settings_kind = self.settings_window.as_ref().map(|(_, panel)| panel.kind());
539-
let settings_task =
540-
settings_kind.map_or_else(Task::none, |kind| self.open_settings_window(kind, true));
541-
Task::batch([
542-
open_main,
543-
window::close(old_main_id),
544-
settings_task,
545-
self.recreate_popout_windows(use_decorations),
546-
])
547-
}
548534
}

0 commit comments

Comments
 (0)