name: robius-widget-patterns description: | 关键:用于罗比乌斯部件模式。触发条件: apply_over, TextOrImage, modal, 可复用, 模态, collapsible, drag drop, reusable widget, widget design, pageflip, 组件设计, 组件模式
罗比乌斯部件模式技能
基于Robrix和Moly代码库模式的Makepad可复用部件设计最佳实践。
源代码库:
- Robrix:Matrix聊天客户端 - Avatar、RoomsList、RoomScreen部件
- Moly:AI聊天应用 - Slot、ChatLine、PromptInput、AdaptiveView部件
触发条件
在以下情况使用此技能:
- 创建可复用的Makepad部件
- 设计部件组件API
- 实现文本/图像切换模式
- Makepad中的动态样式
- 关键词:robrix部件、makepad组件、可复用部件、部件设计模式
生产模式
有关生产就绪的部件模式,请参见 _base/ 目录:
| 模式 | 描述 |
|---|---|
| 01-widget-extension | 向部件引用添加辅助方法 |
| 02-modal-overlay | 使用DrawList2d覆盖的弹出框和对话框 |
| 03-collapsible | 可展开/折叠的部分 |
| 04-list-template | 使用LivePtr模板的动态列表 |
| 05-lru-view-cache | 内存高效的视图缓存 |
| 14-callout-tooltip | 带箭头定位的工具提示 |
| 20-redraw-optimization | 高效的重绘模式 |
| 15-dock-studio-layout | IDE风格的可调整面板布局 |
| 16-hover-effect | 使用实例变量的悬停效果 |
| 17-row-based-grid-layout | 动态网格布局 |
| 18-drag-drop-reorder | 拖放部件重排序 |
| 19-pageflip-optimization | PageFlip切换优化,即刻销毁/缓存模式 |
| 21-collapsible-row-portal-list | 在门户列表中自动分组连续项,使用FoldHeader |
| 22-dropdown-overlay | 使用DrawList2d覆盖的下拉弹出框(无布局推送) |
标准部件结构
use makepad_widgets::*;
live_design! {
use link::theme::*;
use link::widgets::*;
pub MyWidget = {{MyWidget}} {
width: Fill, height: Fit,
flow: Down,
// 在DSL中定义的子部件
inner_view = <View> {
// ...
}
}
}
#[derive(Live, LiveHook, Widget)]
pub struct MyWidget {
#[deref] view: View, // 委托给内部View
#[live] some_property: f64, // DSL可配置属性
#[live(100.0)] default_val: f64, // 带默认值
#[rust] internal_state: State, // 仅Rust状态(不在DSL中)
#[animator] animator: Animator, // 用于动画
}
impl Widget for MyWidget {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.view.handle_event(cx, event, scope);
// 自定义事件处理...
}
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
self.view.draw_walk(cx, scope, walk)
}
}
文本/图像切换模式
一个常见的部件模式,用于显示文本或图像(如头像):
live_design! {
pub Avatar = {{Avatar}} {
width: 36.0, height: 36.0,
align: { x: 0.5, y: 0.5 }
flow: Overlay, // 将视图堆叠在彼此顶部
text_view = <View> {
visible: true, // 默认可见
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, // 默认隐藏
img = <Image> {
fit: Stretch,
width: Fill, height: Fill,
}
}
}
}
#[derive(LiveHook, Live, Widget)]
pub struct Avatar {
#[deref] view: View,
#[rust] info: Option<UserInfo>,
}
impl Avatar {
/// 显示文本内容,隐藏图像
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());
// 获取第一个字符
let first_char = utils::first_letter(username.as_ref())
.unwrap_or("?").to_uppercase();
self.label(ids!(text_view.text)).set_text(cx, &first_char);
// 切换可见性
self.view(ids!(text_view)).set_visible(cx, true);
self.view(ids!(img_view)).set_visible(cx, false);
// 应用可选背景颜色
if let Some(color) = bg_color {
self.view(ids!(text_view)).apply_over(cx, live! {
draw_bg: { background_color: (color) }
});
}
}
/// 显示图像内容,隐藏文本
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
}
/// 检查当前显示状态
pub fn status(&mut self) -> DisplayStatus {
if self.view(ids!(img_view)).visible() {
DisplayStatus::Image
} else {
DisplayStatus::Text
}
}
}
使用apply_over的动态样式
在运行时应用动态样式:
// 应用单个属性
self.view(ids!(content)).apply_over(cx, live! {
draw_bg: { color: #ff0000 }
});
// 应用多个属性
self.view(ids!(message)).apply_over(cx, live! {
padding: { left: 20, right: 20 }
margin: { top: 10 }
});
// 使用变量应用
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) }
});
部件引用模式
为外部API实现 *Ref 方法:
impl AvatarRef {
/// 参见[`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);
}
}
/// 参见[`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(())
}
}
}
可折叠/展开模式
live_design! {
pub CollapsibleSection = {{CollapsibleSection}} {
flow: Down,
header = <View> {
cursor: Hand,
icon = <Icon> { }
title = <Label> { text: "Section" }
}
content = <View> {
visible: false,
// 可展开内容在这里
}
}
}
#[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);
// 旋转图标
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);
}
}
加载状态模式
live_design! {
pub LoadableContent = {{LoadableContent}} {
flow: Overlay,
content = <View> {
visible: true,
// 主要内容
}
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 项目模式
对于虚拟列表项目:
live_design! {
pub ItemsList = {{ItemsList}} {
list = <PortalList> {
keep_invisible: false,
auto_tail: false,
width: Fill, height: Fill,
flow: Down,
// 项目模板
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));
// 用数据填充项目
self.populate_item(cx, item, &self.items[item_id]);
item.draw_all(cx, scope);
}
}
}
DrawStep::done()
}
}
最佳实践
- 使用
#[deref]进行委托:委托给内部View以实现标准行为 - 分离DSL属性 (
#[live]) 和Rust状态 (#[rust]) - 实现内部方法和
*Ref包装器 - 使用
apply_over进行动态运行时样式 - 使用
flow: Overlay进行切换/交换模式 - 使用
set_visible()在替代视图之间切换 - 状态更改后始终调用
redraw(cx)
参考文件
references/widget-patterns.md- 额外的部件模式(Robrix)references/styling-patterns.md- 动态样式模式(Robrix)references/moly-widget-patterns.md- Moly特定模式Slot部件用于运行时内容替换MolyRoot条件渲染包装器AdaptiveView用于响应式移动/桌面布局- 聊天行变体(UserLine、BotLine、ErrorLine等)
CommandTextInput带操作按钮- 带单选按钮的侧边栏导航