Skip to content

Commit df20e7c

Browse files
committed
feat(widgets): bring in system status bar, roller pickers, and actionable dialogs with interactive showcase
1 parent 75fb85f commit df20e7c

8 files changed

Lines changed: 1278 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,8 @@ name = "advanced_cinematic_motion_showcase"
132132
path = "examples/basics/advanced_cinematic_motion_showcase.rs"
133133
required-features = ["std"]
134134

135+
[[example]]
136+
name = "wearable_dialogs_pickers_status_showcase"
137+
path = "examples/basics/wearable_dialogs_pickers_status_showcase.rs"
138+
required-features = ["std"]
139+

examples/basics/advanced_cinematic_motion_showcase.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,11 @@ where
385385
let card_h = 130u32;
386386

387387
// Simulate 3D Flip angle [0..PI]
388-
let angle = t * 3.14159f32;
388+
let angle = t * core::f32::consts::PI;
389389
let cos_val = angle.cos().abs(); // perspective foreshortening factor
390390
let current_h = ((card_h as f32) * cos_val).max(12.0) as u32;
391391

392-
let is_front = angle < 1.57079f32;
392+
let is_front = angle < core::f32::consts::FRAC_PI_2;
393393
let card_rect = Rect::new(
394394
center_x - (card_w as i32 / 2),
395395
center_y - (current_h as i32 / 2),
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//! Showcase: Wearable Status Bar, Pickers & Actionable Dialogs
2+
//!
3+
//! Demonstrates:
4+
//! 1. **System Status Bar (`StatusBarWidget`)**: Real-time battery indicator, charging state, Bluetooth, DND, and clock.
5+
//! 2. **Time Picker (`TimePickerWidget`)**: 12h/24h segmented time selector with focus halo and bump animations.
6+
//! 3. **Numeric Range Picker (`NumberPickerWidget`)**: Incremental value roller with units.
7+
//! 4. **Actionable & Confirmation Dialogs (`ActionableDialogWidget`)**: Icon glyphs, multi-line prompts, and action choices.
8+
//!
9+
//! ### Controls:
10+
//! - **[Left / Right]**: Navigate fields within active picker / select dialog button
11+
//! - **[Up / Down]**: Increment / Decrement active picker value
12+
//! - **[Tab]**: Switch active widget focus (Time Picker -> Number Picker -> Dialog)
13+
//! - **[Space]**: Toggle Status Bar mode / trigger selected dialog action
14+
//! - **[Esc / Q]**: Exit
15+
16+
use embedded_graphics_core::{
17+
draw_target::DrawTarget,
18+
geometry::Size,
19+
pixelcolor::{Rgb565, WebColors},
20+
};
21+
use embedded_graphics_simulator::{
22+
OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window, sdl2::Keycode,
23+
};
24+
use embedded_gui::{
25+
framebuffer::Framebuffer,
26+
geometry::Rect,
27+
render::RenderCtx,
28+
widgets::{
29+
ActionableDialogWidget, BatteryState, DialogAction, DialogType, NumberPickerWidget,
30+
StatusBarMode, StatusBarWidget, TimePickerWidget,
31+
},
32+
};
33+
34+
const W: u32 = 240;
35+
const H: u32 = 240;
36+
37+
#[derive(Clone, Copy, PartialEq, Eq)]
38+
enum ActiveFocus {
39+
TimePicker,
40+
NumberPicker,
41+
Dialog,
42+
}
43+
44+
fn main() {
45+
println!("=== embedded-gui: Wearable Status Bar, Pickers & Dialogs Showcase ===");
46+
47+
let res = std::panic::catch_unwind(|| {
48+
run_interactive_window();
49+
});
50+
51+
if res.is_err() {
52+
println!("\n[Notice: SDL2 desktop window could not be opened in current terminal session]");
53+
println!("[Rendering in standalone console simulation mode...]\n");
54+
run_console_showcase();
55+
}
56+
}
57+
58+
fn run_interactive_window() {
59+
let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(W, H));
60+
let settings = OutputSettingsBuilder::new().scale(3).build();
61+
let mut window = Window::new(
62+
"Wearable Status Bar, Pickers & Dialogs (240x240)",
63+
&settings,
64+
);
65+
66+
let mut status_bar = StatusBarWidget::new("10:42");
67+
status_bar.set_battery(88, BatteryState::Charging);
68+
status_bar.bluetooth_connected = true;
69+
status_bar.dnd_active = false;
70+
71+
let mut time_picker = TimePickerWidget::new_12h(10, 42, true);
72+
let mut number_picker = NumberPickerWidget::new(40, 200, 72, "BPM");
73+
number_picker.is_focused = false;
74+
75+
let mut dialog = ActionableDialogWidget::<3>::new(
76+
"HEART RATE LIMIT",
77+
"Threshold exceeded 140 BPM.",
78+
DialogType::Warning,
79+
);
80+
let _ = dialog.add_action(DialogAction::new("SNOOZE", 1));
81+
let _ = dialog.add_action(DialogAction::destructive("DISMISS", 2));
82+
83+
let mut active_focus = ActiveFocus::TimePicker;
84+
let mut dialog_result: Option<&str> = None;
85+
86+
'running: loop {
87+
// Render Frame
88+
display.clear(Rgb565::new(1, 2, 4)).unwrap();
89+
let screen = Rect::new(0, 0, W, H);
90+
let mut ctx = RenderCtx::new(&mut display, screen);
91+
92+
// 1. Render Status Bar at top
93+
let bar_bounds = Rect::new(0, 0, W, status_bar.height as u32);
94+
let _ = status_bar.render(&mut ctx, bar_bounds);
95+
96+
// 2. Render Time Picker
97+
let tp_bounds = Rect::new(12, 28, W - 24, 48);
98+
let _ = time_picker.render(&mut ctx, tp_bounds);
99+
100+
// 3. Render Number Picker
101+
let np_bounds = Rect::new(12, 82, W - 24, 30);
102+
let _ = number_picker.render(&mut ctx, np_bounds);
103+
104+
// 4. Render Actionable Dialog Card
105+
let dialog_bounds = Rect::new(12, 120, W - 24, 90);
106+
let _ = dialog.render(&mut ctx, dialog_bounds);
107+
108+
// Render dialog action status message
109+
if let Some(msg) = dialog_result {
110+
let _ = ctx.draw_text(16, 218, msg, Rgb565::CSS_GREEN);
111+
} else {
112+
let _ = ctx.draw_text(
113+
16,
114+
218,
115+
"TAB: Focus | ARROWS: Adjust",
116+
Rgb565::new(12, 24, 18),
117+
);
118+
}
119+
120+
window.update(&display);
121+
122+
// Decay bump animation
123+
if time_picker.bump_offset_y > 0 {
124+
time_picker.bump_offset_y -= 1;
125+
} else if time_picker.bump_offset_y < 0 {
126+
time_picker.bump_offset_y += 1;
127+
}
128+
129+
// Process Events
130+
for event in window.events() {
131+
match event {
132+
SimulatorEvent::Quit => break 'running,
133+
SimulatorEvent::KeyDown { keycode, .. } => match keycode {
134+
Keycode::Escape | Keycode::Q => break 'running,
135+
Keycode::Tab => {
136+
active_focus = match active_focus {
137+
ActiveFocus::TimePicker => {
138+
number_picker.is_focused = true;
139+
ActiveFocus::NumberPicker
140+
}
141+
ActiveFocus::NumberPicker => {
142+
number_picker.is_focused = false;
143+
ActiveFocus::Dialog
144+
}
145+
ActiveFocus::Dialog => {
146+
number_picker.is_focused = false;
147+
ActiveFocus::TimePicker
148+
}
149+
};
150+
}
151+
Keycode::Left => match active_focus {
152+
ActiveFocus::TimePicker => time_picker.prev_field(),
153+
ActiveFocus::NumberPicker => number_picker.decrement(),
154+
ActiveFocus::Dialog => dialog.select_prev(),
155+
},
156+
Keycode::Right => match active_focus {
157+
ActiveFocus::TimePicker => time_picker.next_field(),
158+
ActiveFocus::NumberPicker => number_picker.increment(),
159+
ActiveFocus::Dialog => dialog.select_next(),
160+
},
161+
Keycode::Up => match active_focus {
162+
ActiveFocus::TimePicker => time_picker.increment_focused(),
163+
ActiveFocus::NumberPicker => number_picker.increment(),
164+
ActiveFocus::Dialog => dialog.select_prev(),
165+
},
166+
Keycode::Down => match active_focus {
167+
ActiveFocus::TimePicker => time_picker.decrement_focused(),
168+
ActiveFocus::NumberPicker => number_picker.decrement(),
169+
ActiveFocus::Dialog => dialog.select_next(),
170+
},
171+
Keycode::Space | Keycode::Return => {
172+
if active_focus == ActiveFocus::Dialog {
173+
dialog_result = match dialog.current_action_id() {
174+
Some(1) => Some("Action: Snoozed alarm for 5 mins"),
175+
Some(2) => Some("Action: Alert dismissed"),
176+
_ => None,
177+
};
178+
} else {
179+
// Cycle status bar mode
180+
status_bar.mode = match status_bar.mode {
181+
StatusBarMode::ClockAndIcons => StatusBarMode::ClockOnly,
182+
StatusBarMode::ClockOnly => StatusBarMode::IconsOnly,
183+
StatusBarMode::IconsOnly => StatusBarMode::ClockAndIcons,
184+
};
185+
}
186+
}
187+
_ => {}
188+
},
189+
_ => {}
190+
}
191+
}
192+
193+
std::thread::sleep(std::time::Duration::from_millis(16));
194+
}
195+
}
196+
197+
fn run_console_showcase() {
198+
let screen = Rect::new(0, 0, W, H);
199+
let mut fb = Framebuffer::<{ 240 * 240 }>::new(W, H);
200+
let mut ctx = RenderCtx::new(&mut fb, screen);
201+
202+
let mut status_bar = StatusBarWidget::new("10:42");
203+
status_bar.set_battery(95, BatteryState::Full);
204+
status_bar.bluetooth_connected = true;
205+
let _ = status_bar.render(&mut ctx, Rect::new(0, 0, W, 20));
206+
207+
let time_picker = TimePickerWidget::new_12h(10, 42, true);
208+
let _ = time_picker.render(&mut ctx, Rect::new(12, 28, W - 24, 48));
209+
210+
let number_picker = NumberPickerWidget::new(40, 200, 72, "BPM");
211+
let _ = number_picker.render(&mut ctx, Rect::new(12, 82, W - 24, 30));
212+
213+
let mut dialog = ActionableDialogWidget::<2>::new(
214+
"CONFIRM SYNC",
215+
"Send 14 activities?",
216+
DialogType::Question,
217+
);
218+
let _ = dialog.add_action(DialogAction::new("CANCEL", 1));
219+
let _ = dialog.add_action(DialogAction::new("SYNC", 2));
220+
let _ = dialog.render(&mut ctx, Rect::new(12, 120, W - 24, 90));
221+
222+
println!("1. Dynamic System Status Bar rendered (10:42, Battery 95% Full, BT connected).");
223+
println!("2. Segmented 12-Hour Time Picker rendered (10:42 PM with active cell focus).");
224+
println!("3. Numeric Range Picker rendered (72 BPM).");
225+
println!("4. Actionable Modal Dialog rendered with 2 buttons (CANCEL, SYNC).");
226+
println!("\nAll wearable status bar, picker, and dialog widgets validated successfully!");
227+
}

src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,13 @@ pub use widget_animation::{
153153
WidgetAnimationError, WidgetAnimator, WidgetKeyframeState, WidgetPropertyKeyframe,
154154
};
155155
pub use widgets::{
156-
ActionBarWidget, ActionMenuError, ActionMenuItem, ActionMenuWidget, ContentIndicatorDirection,
157-
ContentIndicatorWidget, CrumbsIndicatorWidget, NotificationAction, NotificationError,
158-
NotificationPriority, NotificationSheetWidget, PeekBannerWidget, RichTextError,
159-
RichTextNodeWidget, SelectionWidget, TextSpan, TimelineNodeState, TimelineNodeWidget,
156+
ActionBarWidget, ActionMenuError, ActionMenuItem, ActionMenuWidget, ActionableDialogWidget,
157+
BatteryState, ConfirmationDialogWidget, ContentIndicatorDirection, ContentIndicatorWidget,
158+
CrumbsIndicatorWidget, DialogAction, DialogError, DialogType, NotificationAction,
159+
NotificationError, NotificationPriority, NotificationSheetWidget, NumberPickerWidget,
160+
PeekBannerWidget, PickerError, RichTextError, RichTextNodeWidget, SelectionWidget,
161+
StatusBarError, StatusBarMode, StatusBarWidget, TextSpan, TimeFormat, TimePickerField,
162+
TimePickerWidget, TimelineNodeState, TimelineNodeWidget,
160163
};
161164
pub use widgets::{
162165
ChartMode, KeyboardLayout, NotificationLevel, SurfaceState, WidgetKind, WidgetNode,

0 commit comments

Comments
 (0)