可复用部件设计模式技能Skill robius-widget-patterns

这个技能提供了基于Robrix和Moly代码库的Makepad部件设计最佳实践,包括可复用部件、模态对话框、拖放重排序、动态样式应用等多种模式,帮助开发者高效创建和优化UI组件。关键词:Makepad部件、可复用设计、模态、拖放、优化、UI组件、设计模式、前端开发、软件开发。

前端开发 0 次安装 0 次浏览 更新于 3/13/2026

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()
    }
}

最佳实践

  1. 使用 #[deref] 进行委托:委托给内部View以实现标准行为
  2. 分离DSL属性 (#[live]) 和Rust状态 (#[rust])
  3. 实现内部方法和 *Ref 包装器
  4. 使用 apply_over 进行动态运行时样式
  5. 使用 flow: Overlay 进行切换/交换模式
  6. 使用 set_visible() 在替代视图之间切换
  7. 状态更改后始终调用 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 带操作按钮
    • 带单选按钮的侧边栏导航