Skip to content

Commit 88f4db4

Browse files
authored
Merge pull request #68 from devmobasa/pr/configurator-toasts
UI: add configurator toasts and close overlay on config open
2 parents 1d157d8 + 2d2ba14 commit 88f4db4

File tree

6 files changed

+137
-11
lines changed

6 files changed

+137
-11
lines changed

src/backend/wayland/state/render.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ impl WaylandState {
2727
let now = Instant::now();
2828
let highlight_active = self.input_state.advance_click_highlights(now);
2929
let preset_feedback_active = self.input_state.advance_preset_feedback(now);
30+
let ui_toast_active = self.input_state.advance_ui_toast(now);
3031
let mut eraser_pattern: Option<cairo::SurfacePattern> = None;
3132
let mut eraser_bg_color: Option<Color> = None;
3233

@@ -361,6 +362,7 @@ impl WaylandState {
361362
);
362363
}
363364

365+
crate::ui::render_ui_toast(&ctx, &self.input_state, width, height);
364366
crate::ui::render_preset_toast(&ctx, &self.input_state, width, height);
365367

366368
if !self.zoom.active {
@@ -456,6 +458,6 @@ impl WaylandState {
456458
self.render_toolbars(&snapshot);
457459
}
458460

459-
Ok(highlight_active || preset_feedback_active)
461+
Ok(highlight_active || preset_feedback_active || ui_toast_active)
460462
}
461463
}

src/input/state/core/base.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub const MIN_STROKE_THICKNESS: f64 = 1.0;
44
pub const MAX_STROKE_THICKNESS: f64 = 50.0;
55
pub const PRESET_FEEDBACK_DURATION_MS: u64 = 450;
66
pub const PRESET_TOAST_DURATION_MS: u64 = 1300;
7+
pub const UI_TOAST_DURATION_MS: u64 = 5000;
78

89
use super::{
910
index::SpatialGrid,
@@ -90,12 +91,26 @@ pub enum PresetFeedbackKind {
9091
Clear,
9192
}
9293

94+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95+
pub enum UiToastKind {
96+
Info,
97+
Warning,
98+
Error,
99+
}
100+
93101
#[derive(Debug, Clone)]
94102
pub(crate) struct PresetFeedbackState {
95103
pub kind: PresetFeedbackKind,
96104
pub started: Instant,
97105
}
98106

107+
#[derive(Debug, Clone)]
108+
pub(crate) struct UiToastState {
109+
pub kind: UiToastKind,
110+
pub message: String,
111+
pub started: Instant,
112+
}
113+
99114
pub struct InputState {
100115
/// Multi-frame canvas management (transparent, whiteboard, blackboard)
101116
pub canvas_set: CanvasSet,
@@ -215,6 +230,8 @@ pub struct InputState {
215230
pub show_marker_opacity_section: bool,
216231
/// Whether to show preset action toast notifications
217232
pub show_preset_toasts: bool,
233+
/// Pending UI toast (errors/warnings/info)
234+
pub(crate) ui_toast: Option<UiToastState>,
218235
/// Pending delayed history playback state
219236
pub(super) pending_history: Option<DelayedHistory>,
220237
/// Cached layout details for the currently open context menu
@@ -385,6 +402,7 @@ impl InputState {
385402
show_delay_sliders: false, // Default to hidden
386403
show_marker_opacity_section: false,
387404
show_preset_toasts: true,
405+
ui_toast: None,
388406
pending_history: None,
389407
context_menu_layout: None,
390408
spatial_index: None,

src/input/state/core/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ mod tool_controls;
1212
mod utility;
1313

1414
pub use base::{
15-
DrawingState, InputState, PresetAction, PresetFeedbackKind, PRESET_FEEDBACK_DURATION_MS,
16-
PRESET_TOAST_DURATION_MS, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, ZoomAction,
15+
DrawingState, InputState, PresetAction, PresetFeedbackKind, UiToastKind,
16+
PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS, UI_TOAST_DURATION_MS,
17+
MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, ZoomAction,
1718
};
1819
#[allow(unused_imports)]
1920
pub use menus::{ContextMenuEntry, ContextMenuKind, ContextMenuState, MenuCommand};

src/input/state/core/utility.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use super::base::{InputState, PresetAction, ZoomAction};
1+
use super::base::{
2+
InputState, PresetAction, UiToastKind, UiToastState, ZoomAction, UI_TOAST_DURATION_MS,
3+
};
24
use crate::config::Action;
35
use crate::config::Config;
46
use crate::util::Rect;
57
use std::io::ErrorKind;
68
use std::process::{Command, Stdio};
9+
use std::time::{Duration, Instant};
710

811
impl InputState {
912
/// Updates the cached pointer location.
@@ -138,7 +141,7 @@ impl InputState {
138141
self.zoom_scale
139142
}
140143

141-
pub(crate) fn launch_configurator(&self) {
144+
pub(crate) fn launch_configurator(&mut self) {
142145
let binary = std::env::var("WAYSCRIBER_CONFIGURATOR")
143146
.unwrap_or_else(|_| "wayscriber-configurator".to_string());
144147

@@ -153,24 +156,54 @@ impl InputState {
153156
"Launched wayscriber-configurator (binary: {binary}, pid: {})",
154157
child.id()
155158
);
159+
self.should_exit = true;
156160
}
157161
Err(err) => {
158162
if err.kind() == ErrorKind::NotFound {
159163
log::error!(
160164
"Configurator not found (looked for '{binary}'). Install 'wayscriber-configurator' (Arch: yay -S wayscriber-configurator; deb/rpm users: grab the wayscriber-configurator package from the release page) or set WAYSCRIBER_CONFIGURATOR to its path."
161165
);
166+
self.set_ui_toast(
167+
UiToastKind::Warning,
168+
format!("Configurator not found: {binary}"),
169+
);
162170
} else {
163171
log::error!("Failed to launch wayscriber-configurator using '{binary}': {err}");
164172
log::error!(
165173
"Set WAYSCRIBER_CONFIGURATOR to override the executable path if needed."
166174
);
175+
self.set_ui_toast(
176+
UiToastKind::Error,
177+
"Failed to launch configurator (see logs).",
178+
);
167179
}
168180
}
169181
}
170182
}
171183

184+
pub(crate) fn set_ui_toast(&mut self, kind: UiToastKind, message: impl Into<String>) {
185+
self.ui_toast = Some(UiToastState {
186+
kind,
187+
message: message.into(),
188+
started: Instant::now(),
189+
});
190+
self.needs_redraw = true;
191+
}
192+
193+
pub fn advance_ui_toast(&mut self, now: Instant) -> bool {
194+
let duration = Duration::from_millis(UI_TOAST_DURATION_MS);
195+
let Some(toast) = &self.ui_toast else {
196+
return false;
197+
};
198+
if now.saturating_duration_since(toast.started) >= duration {
199+
self.ui_toast = None;
200+
return false;
201+
}
202+
true
203+
}
204+
172205
/// Opens the primary config file using the desktop default application.
173-
pub(crate) fn open_config_file_default(&self) {
206+
pub(crate) fn open_config_file_default(&mut self) {
174207
let path = match Config::get_config_path() {
175208
Ok(p) => p,
176209
Err(err) => {
@@ -201,6 +234,7 @@ impl InputState {
201234
path.display(),
202235
child.id()
203236
);
237+
self.should_exit = true;
204238
}
205239
Err(err) => {
206240
log::error!("Failed to open config file at {}: {}", path.display(), err);

src/input/state/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod tests;
99
#[allow(unused_imports)]
1010
pub use core::{
1111
ContextMenuEntry, ContextMenuKind, ContextMenuState, DrawingState, InputState, PresetAction,
12-
PresetFeedbackKind, PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS,
13-
MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, SelectionState, ZoomAction,
12+
PresetFeedbackKind, UiToastKind, PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS,
13+
UI_TOAST_DURATION_MS, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS, SelectionState, ZoomAction,
1414
};
1515
pub use highlight::ClickHighlightSettings;

src/ui.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ pub mod toolbar;
33
/// UI rendering: status bar, help overlay, visual indicators
44
use crate::config::StatusPosition;
55
use crate::input::{BoardMode, DrawingState, InputState, Tool, state::ContextMenuState};
6-
use crate::input::state::{PresetFeedbackKind, PRESET_TOAST_DURATION_MS};
6+
use crate::input::state::{
7+
PresetFeedbackKind, UiToastKind, PRESET_TOAST_DURATION_MS, UI_TOAST_DURATION_MS,
8+
};
79
use std::f64::consts::{FRAC_PI_2, PI};
810
use std::time::Instant;
911

@@ -21,6 +23,10 @@ const STATUS_BG_WIDTH_PAD: f64 = 10.0;
2123
const STATUS_BG_HEIGHT_PAD: f64 = 8.0;
2224
/// Color indicator dot X offset
2325
const STATUS_DOT_OFFSET_X: f64 = 3.0;
26+
/// Vertical position for UI toasts (percentage of screen height from top)
27+
const UI_TOAST_Y_RATIO: f64 = 0.12;
28+
/// Portion of toast lifetime to keep fully opaque before fading
29+
const UI_TOAST_HOLD_RATIO: f64 = 0.75;
2430
/// Vertical position for preset toast (percentage of screen height from top)
2531
const PRESET_TOAST_Y_RATIO: f64 = 0.2;
2632

@@ -281,7 +287,7 @@ pub fn render_zoom_badge(
281287
};
282288
let padding = 12.0;
283289
let radius = 8.0;
284-
let font_size = 15.0;
290+
let font_size = 16.0;
285291

286292
ctx.select_font_face("Sans", cairo::FontSlant::Normal, cairo::FontWeight::Bold);
287293
ctx.set_font_size(font_size);
@@ -362,7 +368,12 @@ pub fn render_preset_toast(
362368
let center_y = screen_height as f64 * PRESET_TOAST_Y_RATIO;
363369
let y = center_y - height / 2.0;
364370

365-
let fade = (1.0 - progress as f64).clamp(0.0, 1.0);
371+
let fade = if (progress as f64) <= UI_TOAST_HOLD_RATIO {
372+
1.0
373+
} else {
374+
let t = ((progress as f64) - UI_TOAST_HOLD_RATIO) / (1.0 - UI_TOAST_HOLD_RATIO);
375+
(1.0 - t).clamp(0.0, 1.0)
376+
};
366377
let (r, g, b) = match kind {
367378
PresetFeedbackKind::Apply => (0.22, 0.5, 0.9),
368379
PresetFeedbackKind::Save => (0.2, 0.7, 0.4),
@@ -380,6 +391,66 @@ pub fn render_preset_toast(
380391
let _ = ctx.show_text(&label);
381392
}
382393

394+
/// Render a transient UI toast (warnings/errors/info).
395+
pub fn render_ui_toast(
396+
ctx: &cairo::Context,
397+
input_state: &InputState,
398+
screen_width: u32,
399+
screen_height: u32,
400+
) {
401+
let Some(toast) = input_state.ui_toast.as_ref() else {
402+
return;
403+
};
404+
405+
let now = Instant::now();
406+
let duration_secs = UI_TOAST_DURATION_MS as f32 / 1000.0;
407+
let elapsed = now.saturating_duration_since(toast.started);
408+
let progress = (elapsed.as_secs_f32() / duration_secs).clamp(0.0, 1.0);
409+
if progress >= 1.0 {
410+
return;
411+
}
412+
413+
let label = toast.message.as_str();
414+
let font_size = 15.0;
415+
let padding_x = 16.0;
416+
let padding_y = 9.0;
417+
let radius = 10.0;
418+
419+
let extents = text_extents_for(
420+
ctx,
421+
"Sans",
422+
cairo::FontSlant::Normal,
423+
cairo::FontWeight::Bold,
424+
font_size,
425+
label,
426+
);
427+
let width = extents.width() + padding_x * 2.0;
428+
let height = extents.height() + padding_y * 2.0;
429+
let x = (screen_width as f64 - width) / 2.0;
430+
let center_y = screen_height as f64 * UI_TOAST_Y_RATIO;
431+
let y = center_y - height / 2.0;
432+
433+
let fade = (1.0 - progress as f64).clamp(0.0, 1.0);
434+
let (r, g, b) = match toast.kind {
435+
UiToastKind::Info => (0.22, 0.5, 0.9),
436+
UiToastKind::Warning => (0.92, 0.62, 0.18),
437+
UiToastKind::Error => (0.9, 0.3, 0.3),
438+
};
439+
440+
ctx.set_source_rgba(r, g, b, 0.92 * fade);
441+
draw_rounded_rect(ctx, x, y, width, height, radius);
442+
let _ = ctx.fill();
443+
444+
let text_x = x + (width - extents.width()) / 2.0 - extents.x_bearing();
445+
let text_y = y + (height - extents.height()) / 2.0 - extents.y_bearing();
446+
ctx.set_source_rgba(0.0, 0.0, 0.0, 0.55 * fade);
447+
ctx.move_to(text_x + 1.0, text_y + 1.0);
448+
let _ = ctx.show_text(label);
449+
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0 * fade);
450+
ctx.move_to(text_x, text_y);
451+
let _ = ctx.show_text(label);
452+
}
453+
383454
/// Render help overlay showing all keybindings
384455
pub fn render_help_overlay(
385456
ctx: &cairo::Context,

0 commit comments

Comments
 (0)