Skip to content

Commit c73ecc7

Browse files
committed
feat: add timeline relbar, peek banner, notification sheet, action menu, and rich text widgets with showcase
1 parent a069ae3 commit c73ecc7

8 files changed

Lines changed: 785 additions & 2 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,8 @@ name = "relative_anchors_and_flex_justify_showcase"
122122
path = "examples/basics/relative_anchors_and_flex_justify_showcase.rs"
123123
required-features = ["std"]
124124

125+
[[example]]
126+
name = "wearable_os_subsystems_showcase"
127+
path = "examples/basics/wearable_os_subsystems_showcase.rs"
128+
required-features = ["std"]
129+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Showcase: Wearable OS Subsystems & UI Patterns
2+
//!
3+
//! Demonstrates:
4+
//! 1. **Timeline Relationship Bars (`TimelineNodeWidget`)**: Chronological connective bars linking past, active, and future event pins.
5+
//! 2. **Reactive Peek Banners (`PeekBannerWidget`)**: Heads-up reminder ribbon modifying unobstructed screen canvas.
6+
//! 3. **Modal Notification Sheets (`NotificationSheetWidget`)**: Priority alert popups with action choices and auto-dismiss countdowns.
7+
//! 4. **Cascading Action Menus (`ActionMenuWidget`)**: Hierarchical nested action sheets with highlight cursor.
8+
//! 5. **Rich Multi-Span Text Nodes (`RichTextNodeWidget`)**: Formatted inline text tags and badge spans.
9+
10+
use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
11+
use embedded_gui::{
12+
framebuffer::Framebuffer,
13+
geometry::Rect,
14+
render::RenderCtx,
15+
round::UnobstructedArea,
16+
widgets::{
17+
ActionMenuWidget, NotificationPriority, NotificationSheetWidget, PeekBannerWidget,
18+
RichTextNodeWidget, TextSpan, TimelineNodeState, TimelineNodeWidget,
19+
},
20+
};
21+
22+
fn main() {
23+
println!("=== embedded-gui: Wearable OS Subsystems Showcase ===");
24+
25+
let screen = Rect::new(0, 0, 240, 240);
26+
let mut fb = Framebuffer::<{ 240 * 240 }>::new(240, 240);
27+
let mut ctx = RenderCtx::new(&mut fb, screen);
28+
29+
// 1. Timeline Connective RelBar Demonstration
30+
println!("\n1. Timeline Relationship Connector Bars:");
31+
let past_node = TimelineNodeWidget::new(TimelineNodeState::Past);
32+
let mut active_node = TimelineNodeWidget::new(TimelineNodeState::ActiveNow);
33+
active_node.active_color = Rgb565::CSS_ORANGE;
34+
let future_node = TimelineNodeWidget::new(TimelineNodeState::Future);
35+
36+
let past_slot = Rect::new(12, 10, 16, 40);
37+
let active_slot = Rect::new(12, 50, 16, 40);
38+
let future_slot = Rect::new(12, 90, 16, 40);
39+
40+
past_node.render(&mut ctx, past_slot).unwrap();
41+
active_node.render(&mut ctx, active_slot).unwrap();
42+
future_node.render(&mut ctx, future_slot).unwrap();
43+
44+
println!(" Past pin connector: {:?}", past_slot);
45+
println!(" Active NOW pin node: {:?}", active_slot);
46+
println!(" Upcoming pin node: {:?}", future_slot);
47+
48+
// 2. Reactive Peek Banner adapting UnobstructedArea
49+
println!("\n2. Reactive Canvas Peek Banner:");
50+
let mut unobstructed = UnobstructedArea::new(screen);
51+
let peek = PeekBannerWidget::new("TEAM SYNC (10m)");
52+
peek.apply_to_unobstructed_area(&mut unobstructed);
53+
54+
let banner_rect = Rect::new(screen.x, screen.y, screen.w, peek.height as u32);
55+
peek.render(&mut ctx, banner_rect).unwrap();
56+
println!(" Peek banner rendered at: {:?}", banner_rect);
57+
println!(
58+
" Canvas area adjusted to: {:?}",
59+
unobstructed.visible_rect()
60+
);
61+
62+
// 3. Multi-Span Rich Text Flow Node
63+
println!("\n3. Multi-Span Rich Text Node with Badges:");
64+
let mut text_node = RichTextNodeWidget::<4>::new();
65+
text_node
66+
.push_span(TextSpan::badge(
67+
"CRITICAL",
68+
Rgb565::CSS_WHITE,
69+
Rgb565::new(28, 4, 4),
70+
))
71+
.unwrap();
72+
text_node
73+
.push_span(TextSpan::plain("CPU core temp 48C", Rgb565::CSS_WHITE))
74+
.unwrap();
75+
76+
let text_rect = Rect::new(40, 60, 190, 24);
77+
text_node.render(&mut ctx, text_rect).unwrap();
78+
println!(
79+
" Rendered {} styled text spans in {:?}",
80+
text_node.spans.len(),
81+
text_rect
82+
);
83+
84+
// 4. Modal Notification Sheet with Action Buttons & Progress Bar
85+
println!("\n4. Modal Notification Sheet:");
86+
let mut notif = NotificationSheetWidget::<3>::new(
87+
"CALENDAR REMINDER",
88+
"Architecture Design Review @ 10:00",
89+
NotificationPriority::Important,
90+
);
91+
notif.add_action("DISMISS", 101).unwrap();
92+
notif.add_action("SNOOZE", 102).unwrap();
93+
notif.auto_dismiss_progress = 0.75; // 75% timer remaining
94+
95+
let notif_rect = Rect::new(20, 140, 200, 85);
96+
notif.render(&mut ctx, notif_rect).unwrap();
97+
println!(
98+
" Rendered modal notification card with {} actions & 75% countdown timer.",
99+
notif.actions.len()
100+
);
101+
102+
// 5. Cascading Hierarchical Action Menu
103+
println!("\n5. Cascading Action Menu:");
104+
let mut menu = ActionMenuWidget::<4>::new(Some("SYSTEM ACTIONS"));
105+
menu.add_item("Wireless Sync", 1, true).unwrap();
106+
menu.add_item("Do Not Disturb", 2, false).unwrap();
107+
menu.add_item("Power Options", 3, true).unwrap();
108+
menu.selected_index = 0;
109+
110+
let menu_rect = Rect::new(30, 40, 180, 75);
111+
menu.render(&mut ctx, menu_rect).unwrap();
112+
println!(
113+
" Rendered action menu with {} items, highlighted index {}.",
114+
menu.items.len(),
115+
menu.selected_index
116+
);
117+
118+
println!("\nWearable OS subsystems showcase executed and rendered successfully!");
119+
}

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,10 @@ pub use widget_animation::{
153153
WidgetAnimationError, WidgetAnimator, WidgetKeyframeState, WidgetPropertyKeyframe,
154154
};
155155
pub use widgets::{
156-
ActionBarWidget, ContentIndicatorDirection, ContentIndicatorWidget, CrumbsIndicatorWidget,
157-
SelectionWidget,
156+
ActionBarWidget, ActionMenuError, ActionMenuItem, ActionMenuWidget, ContentIndicatorDirection,
157+
ContentIndicatorWidget, CrumbsIndicatorWidget, NotificationAction, NotificationError,
158+
NotificationPriority, NotificationSheetWidget, PeekBannerWidget, RichTextError,
159+
RichTextNodeWidget, SelectionWidget, TextSpan, TimelineNodeState, TimelineNodeWidget,
158160
};
159161
pub use widgets::{
160162
ChartMode, KeyboardLayout, NotificationLevel, SurfaceState, WidgetKind, WidgetNode,

src/widgets/action_menu.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//! Hierarchical Action Menu and Cascading Action Sheets.
2+
//!
3+
//! Provides `ActionMenuWidget` (contextual cascading action menu with submenus and highlight cursor).
4+
5+
use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
6+
use heapless::Vec;
7+
8+
use crate::{
9+
geometry::Rect,
10+
render::RenderCtx,
11+
style::{Border, Style},
12+
widget::{PropertyKey, PropertyValue, Widget},
13+
};
14+
15+
/// Error indicating action menu capacity exceeded.
16+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17+
pub struct ActionMenuError;
18+
19+
/// A single item in an Action Menu.
20+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21+
pub struct ActionMenuItem<'a> {
22+
pub label: &'a str,
23+
pub action_id: u16,
24+
pub is_submenu: bool,
25+
}
26+
27+
/// Cascading Hierarchical Action Menu widget.
28+
#[derive(Clone, Debug, PartialEq)]
29+
pub struct ActionMenuWidget<'a, const MAX_ITEMS: usize = 8> {
30+
pub title: Option<&'a str>,
31+
pub items: Vec<ActionMenuItem<'a>, MAX_ITEMS>,
32+
pub selected_index: usize,
33+
pub background_color: Rgb565,
34+
pub text_color: Rgb565,
35+
pub selected_bg_color: Rgb565,
36+
pub selected_text_color: Rgb565,
37+
pub accent_color: Rgb565,
38+
}
39+
40+
impl<'a, const MAX_ITEMS: usize> ActionMenuWidget<'a, MAX_ITEMS> {
41+
pub const fn new(title: Option<&'a str>) -> Self {
42+
Self {
43+
title,
44+
items: Vec::new(),
45+
selected_index: 0,
46+
background_color: Rgb565::new(3, 6, 9),
47+
text_color: Rgb565::new(31, 63, 31),
48+
selected_bg_color: Rgb565::new(0, 45, 30),
49+
selected_text_color: Rgb565::new(31, 63, 31),
50+
accent_color: Rgb565::new(0, 35, 30),
51+
}
52+
}
53+
54+
pub fn add_item(
55+
&mut self,
56+
label: &'a str,
57+
action_id: u16,
58+
is_submenu: bool,
59+
) -> Result<(), ActionMenuError> {
60+
self.items
61+
.push(ActionMenuItem {
62+
label,
63+
action_id,
64+
is_submenu,
65+
})
66+
.map_err(|_| ActionMenuError)
67+
}
68+
69+
pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
70+
where
71+
D: DrawTarget<Color = Rgb565>,
72+
C: crate::render::Compositor<D>,
73+
{
74+
ctx.fill_rounded_rect(bounds, 4, self.background_color)?;
75+
ctx.stroke_rounded_rect(bounds, 4, Border::one(self.accent_color))?;
76+
77+
let mut y = bounds.y + 4;
78+
79+
// Title (if present)
80+
if let Some(title) = self.title {
81+
ctx.draw_text(bounds.x + 8, y, title, Rgb565::new(15, 30, 20))?;
82+
y += 14;
83+
}
84+
85+
let item_h = 16;
86+
for (i, item) in self.items.iter().enumerate() {
87+
let is_selected = i == self.selected_index;
88+
let item_rect = Rect::new(bounds.x + 4, y, bounds.w.saturating_sub(8), item_h as u32);
89+
90+
if is_selected {
91+
ctx.fill_rounded_rect(item_rect, 2, self.selected_bg_color)?;
92+
}
93+
94+
let fg = if is_selected {
95+
self.selected_text_color
96+
} else {
97+
self.text_color
98+
};
99+
ctx.draw_text(bounds.x + 10, y + 3, item.label, fg)?;
100+
101+
// Submenu chevron hint '>'
102+
if item.is_submenu {
103+
ctx.draw_text(bounds.right() - 14, y + 3, ">", fg)?;
104+
}
105+
106+
y += item_h + 2;
107+
}
108+
109+
Ok(())
110+
}
111+
}
112+
113+
impl<'a, const MAX_ITEMS: usize> Widget for ActionMenuWidget<'a, MAX_ITEMS> {
114+
fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
115+
116+
fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
117+
match key {
118+
PropertyKey::Selected => Some(PropertyValue::Int(self.selected_index as i32)),
119+
_ => None,
120+
}
121+
}
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
use crate::framebuffer::Framebuffer;
128+
129+
#[test]
130+
fn test_action_menu_render() {
131+
let mut menu = ActionMenuWidget::<4>::new(Some("SETTINGS"));
132+
assert!(menu.add_item("Wi-Fi", 1, true).is_ok());
133+
assert!(menu.add_item("Bluetooth", 2, true).is_ok());
134+
assert!(menu.add_item("Restart", 3, false).is_ok());
135+
136+
let mut fb = Framebuffer::<24000>::new(160, 100);
137+
let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 160, 100));
138+
assert!(menu.render(&mut ctx, Rect::new(0, 0, 160, 100)).is_ok());
139+
}
140+
}

src/widgets/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ use crate::{
1717
},
1818
};
1919

20+
pub mod action_menu;
2021
pub mod basic;
2122
pub mod cinematic;
2223
pub mod controls;
2324
pub mod data;
2425
pub mod gauges;
26+
pub mod notification;
27+
pub mod rich_text_node;
28+
pub mod timeline;
2529
pub mod wearable;
2630

31+
pub use action_menu::{ActionMenuError, ActionMenuItem, ActionMenuWidget};
2732
pub use basic::{ButtonWidget, LabelWidget, PanelWidget, SpacerWidget};
2833
pub use cinematic::GlanceTileWidget;
2934
pub use controls::{CheckboxWidget, SliderWidget, ToggleWidget};
3035
pub use data::ListWidget;
3136
pub use gauges::ProgressBarWidget;
37+
pub use notification::{
38+
NotificationAction, NotificationError, NotificationPriority, NotificationSheetWidget,
39+
};
40+
pub use rich_text_node::{RichTextError, RichTextNodeWidget, TextSpan};
41+
pub use timeline::{PeekBannerWidget, TimelineNodeState, TimelineNodeWidget};
3242
pub use wearable::{
3343
ActionBarWidget, ContentIndicatorDirection, ContentIndicatorWidget, CrumbsIndicatorWidget,
3444
SelectionWidget,

0 commit comments

Comments
 (0)