6 - 添加状态

在之前的几个步骤中,我们为应用程序创建了一个图像网格的初始实现。

为了保持初始实现的简单性,我们的图像网格存在以下限制:

  • 行数是固定的。
  • 每行的项目数量是固定的。
  • 使用的是占位图像,而不是真实图像。

在接下来的几个步骤中,我们将去除这些限制,使图像网格变得更加动态。

我们的计划如下:

  • 首先,我们将为应用程序添加状态。
  • 然后,我们将使用这些状态来加载真实图像。

在本步骤中,我们将从为应用程序添加状态开始。

注意: 如果你不想手动输入代码,可以在此处找到本步骤的全部代码:https://github.com/makepad/image_viewer/tree/main/step_6

你将学到什么

在本步骤中,你将学习:

  • 如何为你的应用程序添加状态。
  • 如何在小部件(widgets)中使用这些状态。

定义 State 结构体

我们首先定义一个结构体,用于存储图像网格所需的状态。

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

1#[derive(Debug)]
2pub struct State {
3    num_images: usize,
4    max_images_per_row: usize,
5}

这段代码定义了一个名为 State 的结构体,其中包含以下两个字段:

  • num_images:表示图像的总数。
  • max_images_per_row:表示每行最多显示的图像数量。

这些就是绘制图像网格所需的全部状态。

实现 Default trait

接下来,我们将为 State 结构体实现 Default trait。
State 结构体定义 Default trait 可以让它作为 App 结构体中的一个字段使用。

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

1impl Default for State {
2    fn default() -> Self {
3        Self {
4            num_images: 12,
5            max_images_per_row: 4,
6        }
7    }
8}

这段代码将创建一个 State 实例,其字段值与我们之前硬编码的值相同:

  • 每行 4 张图片
  • 每个网格 3 行图片

添加辅助函数

为了让后续的代码更易于编写,我们将在 State 结构体中添加一些辅助函数。

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

1impl State {
2    fn num_images(&self) -> usize {
3        self.num_images
4    }
5
6    fn num_rows(&self) -> usize {
7        self.num_images().div_ceil(self.max_images_per_row)
8    }
9
10    fn first_image_idx_for_row(&self, row_idx: usize) -> usize {
11        row_idx * self.max_images_per_row
12    }
13
14    fn num_images_for_row(&self, row_idx: usize) -> usize {
15        let first_image_idx = self.first_image_idx_for_row(row_idx);
16        let num_remaining_images = self.num_images() - first_image_idx;
17        self.max_images_per_row.min(num_remaining_images)
18    }
19}

这些方法的作用如下,应该比较容易理解:

  • num_images:返回图像的总数量。
  • num_rows:返回总的行数。
  • first_image_idx(row_idx):返回指定行的第一张图像的索引。
  • num_images_for_row(row_idx):返回指定行的图像数量。

注意: 通常每行的图像数量由 State 结构体中的 max_images_per_row 字段决定,但在最后一行中,如果剩余的图像数量少于该值,那么这一行的图像数量也会相应减少。

更新 App 结构体

现在我们已经创建了 State 结构体,接下来将它添加到 App 结构体中。

app.rs 中,用下面的定义替换 live design 块中的 App 定义:

1#[derive(Live, LiveHook)]
2pub struct App {
3    #[live]
4    ui: WidgetRef,
5    #[rust]
6    state: State,
7}

这将为 App 结构体添加一个名为 state 的字段,其类型为 State 结构体的实例。

请注意,我们使用了 #[rust] 属性来标记 state 字段为普通的 Rust 字段。回忆一下,当 live design 系统遇到带有 #[rust] 标记的字段时,它会使用该类型的 Default::default 构造函数来初始化该字段。这正是我们之前为 State 结构体实现 Default trait 的原因。

在 Widgets 中使用 State

现在我们已经有了用于存储应用状态的位置,接下来来看看如何在应用中的小部件(widgets)中使用这个状态。

在 App 中使用 State

我们现在面临的直接问题是:状态保存在 App 结构体中,但我们的各个小部件却无法访问这个状态。为了让小部件能够访问状态,我们需要将状态传递给整个小部件树(widget tree)。

让我们先来看一下当前 App 结构体中 AppMain trait 的实现:

1impl AppMain for App {
2    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
3        let mut scope = Scope::with_data(&mut self.state);
4        self.ui.handle_event(cx, event, &mut scope);
5    }
6}

注意我们目前使用的是 Scope::empty,为每个事件创建了一个空的作用域(scope)。回忆一下,在 Makepad 中,scope 是一个容器,它用于在事件传递过程中传递应用级数据(可变的)以及小部件的 props(不可变的)。

之前,我们还没有状态,因此只是简单地创建了空的作用域。而现在我们有了实际的状态数据,是时候做出修改了。

app.rs 中,用下面的代码替换 AppMain trait 的实现:

1impl AppMain for App {
2    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
3	    let mut scope = Scope::with_data(&mut self.state);
4        self.ui.handle_event(cx, event, &mut scope);
5    }
6}

这段代码使用 Scope::with_data 创建了一个包含对 state 字段的可变引用的新作用域。然后将这个作用域传递给我们小部件树的根部。从这里开始,它会自动向下传递到 ImageGrid,我们就可以在其中访问状态;接着,状态还会进一步传递到 ImageRow 中。

在 ImageGrid 中使用 State

接下来,我们将在 ImageGrid 中使用状态。

app.rs 中,用下面的代码替换 ImageGrid 结构体中 Widget trait 的 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                let state = scope.data.get_mut::<State>().unwrap();
10
11                list.set_item_range(cx, 0, state.num_rows());
12                while let Some(row_idx) = list.next_visible_item(cx) {
13                    if row_idx >= state.num_rows() {
14                        continue;
15                    }
16   
17                    let row = list.item(cx, row_idx, live_id!(ImageRow));
18                    let mut scope = Scope::with_data_props(state, &row_idx);
19                    row.draw_all(cx, &mut scope);
20                }
21            }
22        }
23        DrawStep::done()
24    }

在这个新实现中,我们首先从作用域(scope)中获取状态:

1let state = scope.data.get_mut::<State>().unwrap();

接着,我们从状态中获取行数,并告诉列表我们需要这么多项目:

1list.set_item_range(cx, 0, state.num_rows());

最后,我们使用 Scope::with_data_props 创建一个新的作用域,其中包含对状态的可变引用以及当前行索引的引用,然后将这个新的作用域传递给当前的 ImageRow

1let mut scope = Scope::with_data_props(state, &row_idx);
2row.draw_all(cx, &mut scope);

下面的检查:

1if row_idx >= state.num_rows() {
2    continue;
3}

是必要的,因为 set_item_range 只是指定了项目的最小数量:next_visible_item 可能会返回更多的项目,如果界面空间允许的话。当我们只绘制占位图时,这不会有问题,但现在我们是根据索引访问数组元素,如果项目数量超出预期,可能会导致数组越界错误,所以必须对这个情况进行保护。

在 ImageRow 中使用 State

最后,我们将在 ImageRow 中使用状态。

app.rs 中,用下面的代码替换 ImageRow 结构体中 Widget trait 的 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                let state = scope.data.get_mut::<State>().unwrap();
10                let row_idx = *scope.props.get::<usize>().unwrap();
11
12                list.set_item_range(cx, 0, state.num_images_for_row(row_idx));
13                while let Some(item_idx) = list.next_visible_item(cx) {
14                    if item_idx >= state.num_images_for_row(row_idx) {
15                        continue;
16                    }
17
18                    let item = list.item(cx, item_idx, live_id!(ImageItem));
19                    item.draw_all(cx, &mut Scope::empty());
20                }
21            }
22        }
23        DrawStep::done()
24    }

在这个新实现中,我们首先从作用域(scope)中获取状态和当前行索引:

1let state = scope.data.get_mut::<State>().unwrap();
2let row_idx = *scope.props.get::<usize>().unwrap();

接着,我们从状态中获取当前行的图像数量,并告诉列表我们需要这么多项目:

1list.set_item_range(cx, 0, state.num_images_for_row(row_idx));

和之前一样,下面的检查:

1if item_idx >= state.num_images_for_row(row_idx) {
2    continue;
3}

是必须的,用来确保项目数量不会超过预期(因为 set_item_range 只指定了项目的最小数量)。

检查到目前为止的进度

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

确保你当前处于你的包目录下,然后运行以下命令:

1cargo run --release

如果一切正常,你的屏幕上应该会出现一个窗口,里面显示的占位图像网格与之前一样:

不同的是,现在图像网格是基于状态动态绘制的,而不是硬编码的值。为了说明这一点,我们暂时将 State 结构体中 Default trait 的实现改成如下代码:

1impl Default for State {
2    fn default() -> Self {
3        Self {
4            num_images: 7,
5            max_images_per_row: 4,
6        }
7    }
8}

重新编译后,屏幕上显示的占位图像网格应该会变成如下样子:

下一步

我们现在已经拥有了绘制图像网格所需的状态。下一步,我们将使用这些状态来加载真实的图片。