@@ -3,7 +3,9 @@ pub mod toolbar;
33/// UI rendering: status bar, help overlay, visual indicators
44use crate :: config:: StatusPosition ;
55use 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+ } ;
79use std:: f64:: consts:: { FRAC_PI_2 , PI } ;
810use std:: time:: Instant ;
911
@@ -21,6 +23,10 @@ const STATUS_BG_WIDTH_PAD: f64 = 10.0;
2123const STATUS_BG_HEIGHT_PAD : f64 = 8.0 ;
2224/// Color indicator dot X offset
2325const 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)
2531const 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
384455pub fn render_help_overlay (
385456 ctx : & cairo:: Context ,
0 commit comments