4 - 创建 ImageRow

在上一步中,我们创建了一个 ImageItem 来显示单张图片,作为构建应用图片网格的一部分。

回顾一下我们的图片网格结构:

  • 每张图片存储在一个 ImageItem 中。
  • 多个 ImageItem 水平排列在一个 ImageRow 中。
  • 多个 ImageRow 垂直排列组成一个 ImageGrid

这一步中,我们将把多个 ImageItem 组合到一个 ImageRow 中,以显示一行图片。

注意:如果你不想跟着敲代码,可以在这里找到本步骤的全部代码:https://github.com/makepad/image_viewer/tree/main/step_4

你将学到的内容

在本步骤中,你将学习:

  • 如何将项目水平排列。
  • 如何使用 PortalList 来显示一组项目。
  • 如何使用 Rust 代码来自定义一个组件的行为。
  • 如何使用 Rust 代码生成 PortalList 的内容。

定义ImageRow

我们首先添加一个 ImageRow 的定义。ImageRow 负责将多个 ImageItem 水平排列。

app.rs 中,将以下代码添加到 live design 块中,紧跟在 ImageItem 的定义之后:

1ImageRow = {{ImageRow}} {
2        <PortalList> {
3            height: 256,
4            flow: Right,
5            
6            ImageItem = <ImageItem> {}
7        }
8	}

定义 ImageRow

这段代码定义了一个 ImageRow{{ImageRow}} 语法将我们在 DSL 中定义的 ImageRow 关联到 Rust 代码中的 ImageRow 结构体(我们稍后会介绍这个结构体)。

ImageRow 内部,我们使用了一个 PortalList 来列出其中的条目。

这个 PortalList 具有以下属性:

  • height: 256:确保列表具有足够的垂直空间来显示每一个条目。
  • flow: Right:确保列表的子元素从左到右排列。

注意PortalList 类似于一个标准列表,但它支持“无限滚动”:它只会渲染可见的条目,因此可以高效地处理大型列表。尽管我们其实不需要无限滚动的功能,但在撰写本文时,Makepad 还没有标准列表组件,因此我们使用 PortalList 作为替代方案。

模板(Templates)

与其他组件不同,PortalList 的内容并不是通过 DSL 代码来决定的,而是必须通过 Rust 代码动态生成。

因此,下面这一行代码:

1ImageItem = <ImageItem> {}

它并不像你可能预期的那样定义了一个 ImageItem 实例。相反,它定义了一个 ImageItem 的模板。稍后,当我们在 Rust 代码中生成这个 PortalList 的内容时,就可以使用这个模板来创建所需的各个项的实例。

注意: 回忆一下,{{ImageRow}} 语法告诉 Makepad,ImageRow 的定义关联到了 Rust 代码中的一个 ImageRow 结构体。正因为 PortalList 的内容必须由 Rust 代码生成,我们才需要使用一个 Rust 结构体来承载它。

更新 App

现在我们已经定义了 ImageRow,接下来我们将更新 App 的定义,使其显示一个 ImageRow,而不是一个 ImageItem

app.rs 中,将 live design 块中的 App 定义替换为下面这一段代码:

1App = {{App}} {
2        ui: <Root> {
3            <Window> {
4                body = <View> {
5                    <ImageRow> {}
6                }
7            }
8        }
9    }

定义ImageRow 结构体

在之前的 DSL 代码中,我们使用 {{ImageRow}} 语法将 ImageRow 链接到了 Rust 代码中的 ImageRow 结构体。通过定义这样一个结构体,我们可以使用 Rust 代码来重写 ImageRow 的行为。

将以下代码添加到 app.rs 中:

1#[derive(Live, LiveHook, Widget)]
2pub struct ImageRow {
3    #[deref]
4    view: View,
5}

请注意,我们为 ImageRow 结构体派生了多个 trait。我们已经了解了 LiveLiveHook 这两个 trait 的作用,但 Widget 是新的。我们来更详细地看看这个 trait 的作用。

派生Widget trait

Widget trait 允许我们自定义一个小部件(widget)的行为。

有些令人困惑的是,派生 Widget trait 并不会自动生成该 trait 的实现。相反,它会为 Widget 所依赖的一些辅助 trait 生成实现。这使得我们更容易手动实现 Widget trait,但我们仍然需要自己编写具体的实现代码,我们将在下一节中完成这部分工作。

#[deref] 属性在派生 Widget trait 时会用到。将此属性放在 ImageRow 结构体的 view 字段上,可以让我们像使用 View 一样使用 ImageRow:派生 Widget trait 会自动生成将 ImageRow 解引用为 View 的代码,并建立相应的 DSL 绑定。

Widget trait的实现

增加这段代码到 app.rs :

1impl Widget for ImageRow {
2    fn draw_walk(
3        &mut self,
4        cx: &mut Cx2d,
5        scope: &mut Scope,
6        walk: Walk,
7    ) -> DrawStep {
8        while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
9            if let Some(mut list) = item.as_portal_list().borrow_mut() {
10                list.set_item_range(cx, 0, 4);
11                while let Some(item_idx) = list.next_visible_item(cx) {
12                    let item = list.item(cx, item_idx, live_id!(ImageItem));
13                    item.draw_all(cx, &mut Scope::empty());
14                }
15            }
16        }
17        DrawStep::done()
18    }
19
20    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
21        self.view.handle_event(cx, event, scope)
22    }
23}

Widget trait 包含两个方法:

  • handle_event 方法控制 ImageRow 如何响应事件。目前我们不需要对事件进行自定义处理,因此我们只是将所有事件转发给 view
  • draw_walk 方法控制小部件的绘制方式。由于 ImageRow 中的 PortalList 内容必须在 Rust 代码中定义,因此我们确实需要自定义绘制逻辑,所以该方法的实现会稍微复杂一些。下面我们将详细介绍这个实现。

View 中绘制每个项目

让我们仔细看看 draw_walk 方法:

1fn draw_walk(
2        &mut self,
3        cx: &mut Cx2d,
4        scope: &mut Scope,
5        walk: Walk,
6    ) -> DrawStep {
7        while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
8            if let Some(mut list) = item.as_portal_list().borrow_mut() {
9                list.set_item_range(cx, 0, 4);
10                while let Some(item_index) = list.next_visible_item(cx) {
11                    let item = list.item(cx, item_index, live_id!(ImageItem));
12                    item.draw_all(cx, &mut Scope::empty());
13                }
14            }
15        }
16        DrawStep::done()
17    }

draw_walk 方法内部,我们首先在一个循环中调用 self.view.draw_walk(cx, scope, walk) 来绘制视图中的每个项目。

view 上的 draw_walk 函数是一个所谓的可恢复函数(resumable function)——它的行为类似于迭代器,在绘制过程中逐个生成项目。

每次调用 draw_walk,它会返回一个特殊的 DrawStep 对象,表示绘制过程的当前状态。然后我们调用该对象的 step 方法,获取下一个应该绘制的项目。调用者(也就是我们)负责绘制每个项目,这使得我们可以自定义它的绘制方式。

当没有更多项目需要绘制时,调用 step 方法会返回 None,循环结束。

绘制PortalList

draw_walk 方法中以下代码负责绘制 PortalList(回想一下,在 ImageRow 中,我们使用 PortalList 来列出其项目。)

1if let Some(mut list) = item.as_portal_list().borrow_mut() {
2        list.set_item_range(cx, 0, 4);
3        while let Some(item_index) = list.next_visible_item(cx) {
4            let item = list.item(cx, item_index, live_id!(ImageItem));
5            item.draw_all(cx, &mut Scope::empty());
6        }
7    }

对于每个要绘制的项目,我们首先调用 as_portal_list 来检查该项目是否是一个 PortalList。一旦获得对 PortalList 的引用:

  • 我们首先调用 list.set_item_range(cx, 0, 4),告诉列表我们需要 4 个项目。
  • 然后调用 next_visible_item(cx) 来迭代每个可见项目的索引。

对于每个索引:

  • 我们首先调用 list.item(...) 来获取该索引处项目的引用。
  • 然后调用 item.draw_all(...) 将该项目绘制到屏幕上。

每个 PortalList 有自己独立的索引命名空间 —— 这意味着每个列表中每个项目的索引是唯一的。调用 list.item(cx, item_index, live_id!(ImageItem)) 会检查给定索引是否已有对应的项目实例。如果没有,它会创建该项目的实例。

那么,调用 list.item(cx, item_index, live_id!(ImageItem)) 是如何知道要实例化什么呢?很简单:它使用我们之前在 DSL 中定义的 ImageItem 模板:

1ImageItem = <ImageItem> {}

注意live_id! 宏用于在 Makepad 中生成唯一标识符。在这里,live_id!(ImageItem) 指的是 ImageItem 的模板。

检查到目前为止的进度

让我们检查一下目前为止的进展。

确保你在你的包目录中,然后运行:

1cargo run --release

如果一切正常,屏幕上应该会出现一个包含一排占位符图片的窗口:

下一步

在这一步中,我们创建了一个 ImageRow 用来显示一排图片。下一步,我们将把多个 ImageRow 组合到一个 ImageGrid 中,以显示一个图片网格。