Robius Widget Patterns Skill
Best practices for designing reusable Makepad widgets based on Robrix and Moly codebase patterns.
Source codebases:
- Robrix: Matrix chat client - Avatar, RoomsList, RoomScreen widgets
- Moly: AI chat application - Slot, ChatLine, PromptInput, AdaptiveView widgets
Triggers
Use this skill when:
- Creating reusable Makepad widgets
- Designing widget component APIs
- Implementing text/image toggle patterns
- Dynamic styling in Makepad
- Keywords: robrix widget, makepad component, reusable widget, widget design pattern
Production Patterns
For production-ready widget patterns, see the _base/ directory:
| Pattern | Description |
|---|---|
| 01-widget-extension | Add helper methods to widget references |
| 02-modal-overlay | Popups, dialogs using DrawList2d overlay |
| 03-collapsible | Expandable/collapsible sections |
| 04-list-template | Dynamic lists with LivePtr templates |
| 05-lru-view-cache | Memory-efficient view caching |
| 14-callout-tooltip | Tooltips with arrow positioning |
| 20-redraw-optimization | Efficient redraw patterns |
| 15-dock-studio-layout | IDE-style resizable panels |
| 16-hover-effect | Hover effects with instance variables |
| 17-row-based-grid-layout | Dynamic grid layouts |
| 18-drag-drop-reorder | Drag-and-drop widget reordering |
| 19-pageflip-optimization | PageFlip 切换优化,即刻销毁/缓存模式 |
Standard Widget Structure
use makepad_widgets::*; live_design! { use link::theme::*; use link::widgets::*; pub MyWidget = {{MyWidget}} { width: Fill, height: Fit, flow: Down, // Child widgets defined in DSL inner_view = <View> { // ... } } } #[derive(Live, LiveHook, Widget)] pub struct MyWidget { #[deref] view: View, // Delegate to inner View #[live] some_property: f64, // DSL-configurable property #[live(100.0)] default_val: f64, // With default value #[rust] internal_state: State, // Rust-only state (not in DSL) #[animator] animator: Animator, // For animations } impl Widget for MyWidget { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); // Custom event handling... } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { self.view.draw_walk(cx, scope, walk) } }
Text/Image Toggle Pattern
A common pattern for widgets that show either text or an image (like avatars):
live_design! { pub Avatar = {{Avatar}} { width: 36.0, height: 36.0, align: { x: 0.5, y: 0.5 } flow: Overlay, // Stack views on top of each other text_view = <View> { visible: true, // Default visible show_bg: true, draw_bg: { uniform background_color: #888888 fn pixel(self) -> vec4 { let sdf = Sdf2d::viewport(self.pos * self.rect_size); let c = self.rect_size * 0.5; sdf.circle(c.x, c.x, c.x) sdf.fill_keep(self.background_color); return sdf.result } } text = <Label> { text: "?" } } img_view = <View> { visible: false, // Hidden by default img = <Image> { fit: Stretch, width: Fill, height: Fill, } } } } #[derive(LiveHook, Live, Widget)] pub struct Avatar { #[deref] view: View, #[rust] info: Option<UserInfo>, } impl Avatar { /// Show text content, hiding the image pub fn show_text<T: AsRef<str>>( &mut self, cx: &mut Cx, bg_color: Option<Vec4>, info: Option<AvatarTextInfo>, username: T, ) { self.info = info.map(|i| i.into()); // Get first character let first_char = utils::first_letter(username.as_ref()) .unwrap_or("?").to_uppercase(); self.label(ids!(text_view.text)).set_text(cx, &first_char); // Toggle visibility self.view(ids!(text_view)).set_visible(cx, true); self.view(ids!(img_view)).set_visible(cx, false); // Apply optional background color if let Some(color) = bg_color { self.view(ids!(text_view)).apply_over(cx, live! { draw_bg: { background_color: (color) } }); } } /// Show image content, hiding the text pub fn show_image<F, E>( &mut self, cx: &mut Cx, info: Option<AvatarImageInfo>, image_set_fn: F, ) -> Result<(), E> where F: FnOnce(&mut Cx, ImageRef) -> Result<(), E> { let img_ref = self.image(ids!(img_view.img)); let res = image_set_fn(cx, img_ref); if res.is_ok() { self.view(ids!(img_view)).set_visible(cx, true); self.view(ids!(text_view)).set_visible(cx, false); self.info = info.map(|i| i.into()); } res } /// Check current display status pub fn status(&mut self) -> DisplayStatus { if self.view(ids!(img_view)).visible() { DisplayStatus::Image } else { DisplayStatus::Text } } }
Dynamic Styling with apply_over
Apply dynamic styles at runtime:
// Apply single property self.view(ids!(content)).apply_over(cx, live! { draw_bg: { color: #ff0000 } }); // Apply multiple properties self.view(ids!(message)).apply_over(cx, live! { padding: { left: 20, right: 20 } margin: { top: 10 } }); // Apply with variables let highlight_color = if is_selected { vec4(1.0, 0.0, 0.0, 1.0) } else { vec4(0.5, 0.5, 0.5, 1.0) }; self.view(ids!(item)).apply_over(cx, live! { draw_bg: { color: (highlight_color) } });
Widget Reference Pattern
Implement *Ref methods for external API:
impl AvatarRef { /// See [`Avatar::show_text()`]. pub fn show_text<T: AsRef<str>>( &self, cx: &mut Cx, bg_color: Option<Vec4>, info: Option<AvatarTextInfo>, username: T, ) { if let Some(mut inner) = self.borrow_mut() { inner.show_text(cx, bg_color, info, username); } } /// See [`Avatar::show_image()`]. pub fn show_image<F, E>( &self, cx: &mut Cx, info: Option<AvatarImageInfo>, image_set_fn: F, ) -> Result<(), E> where F: FnOnce(&mut Cx, ImageRef) -> Result<(), E> { if let Some(mut inner) = self.borrow_mut() { inner.show_image(cx, info, image_set_fn) } else { Ok(()) } } }
Collapsible/Expandable Pattern
live_design! { pub CollapsibleSection = {{CollapsibleSection}} { flow: Down, header = <View> { cursor: Hand, icon = <Icon> { } title = <Label> { text: "Section" } } content = <View> { visible: false, // Expandable content here } } } #[derive(Live, LiveHook, Widget)] pub struct CollapsibleSection { #[deref] view: View, #[rust] is_expanded: bool, } impl CollapsibleSection { pub fn toggle(&mut self, cx: &mut Cx) { self.is_expanded = !self.is_expanded; self.view(ids!(content)).set_visible(cx, self.is_expanded); // Rotate icon let rotation = if self.is_expanded { 90.0 } else { 0.0 }; self.view(ids!(header.icon)).apply_over(cx, live! { draw_icon: { rotation: (rotation) } }); self.redraw(cx); } }
Loading State Pattern
live_design! { pub LoadableContent = {{LoadableContent}} { flow: Overlay, content = <View> { visible: true, // Main content } loading_overlay = <View> { visible: false, show_bg: true, draw_bg: { color: #00000088 } align: { x: 0.5, y: 0.5 } <BouncingDots> { } } error_view = <View> { visible: false, error_label = <Label> { } } } } #[derive(Live, LiveHook, Widget)] pub struct LoadableContent { #[deref] view: View, #[rust] state: LoadingState, } pub enum LoadingState { Idle, Loading, Loaded, Error(String), } impl LoadableContent { pub fn set_state(&mut self, cx: &mut Cx, state: LoadingState) { self.state = state; match &self.state { LoadingState::Idle | LoadingState::Loaded => { self.view(ids!(content)).set_visible(cx, true); self.view(ids!(loading_overlay)).set_visible(cx, false); self.view(ids!(error_view)).set_visible(cx, false); } LoadingState::Loading => { self.view(ids!(content)).set_visible(cx, true); self.view(ids!(loading_overlay)).set_visible(cx, true); self.view(ids!(error_view)).set_visible(cx, false); } LoadingState::Error(msg) => { self.view(ids!(content)).set_visible(cx, false); self.view(ids!(loading_overlay)).set_visible(cx, false); self.view(ids!(error_view)).set_visible(cx, true); self.label(ids!(error_view.error_label)).set_text(cx, msg); } } self.redraw(cx); } }
PortalList Item Pattern
For virtual list items:
live_design! { pub ItemsList = {{ItemsList}} { list = <PortalList> { keep_invisible: false, auto_tail: false, width: Fill, height: Fill, flow: Down, // Item templates item_entry = <ItemEntry> {} header = <SectionHeader> {} empty = <View> {} } } } impl Widget for ItemsList { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { while let Some(item) = self.view.draw_walk(cx, scope, walk).step() { if let Some(mut list) = item.as_portal_list().borrow_mut() { list.set_item_range(cx, 0, self.items.len()); while let Some(item_id) = list.next_visible_item(cx) { let item = list.item(cx, item_id, live_id!(item_entry)); // Populate item with data self.populate_item(cx, item, &self.items[item_id]); item.draw_all(cx, scope); } } } DrawStep::done() } }
Best Practices
- Use
#[deref]for delegation: Delegate to inner View for standard behavior - Separate DSL properties (
#[live]) from Rust state (#[rust]) - Implement both inner methods and
*Refwrappers - Use
apply_overfor dynamic runtime styling - Use
flow: Overlayfor toggle/swap patterns - Use
set_visible()to toggle between alternative views - Always call
redraw(cx)after state changes
Reference Files
references/widget-patterns.md- Additional widget patterns (Robrix)references/styling-patterns.md- Dynamic styling patterns (Robrix)references/moly-widget-patterns.md- Moly-specific patternsSlotwidget for runtime content replacementMolyRootconditional rendering wrapperAdaptiveViewfor responsive Mobile/Desktop layouts- Chat line variants (UserLine, BotLine, ErrorLine, etc.)
CommandTextInputwith action buttons- Sidebar navigation with radio buttons