19 - 拆分模块

在上一步中,我们实现了一个模态窗,并成功的把模态窗的触发与关闭绑定到了对应的按钮/事件上。同时,我们的应用代码都在 app.rs 下,显得十分冗长,难以维护。在这一步中,我们将会将应用拆成几个不同的组件,并且放入不同的模块中。

你将学到的内容

  • 如何模块化构建 Makepad 项目。

注意: 如果你不想自己敲代码,可以在这里找到本步骤的完整代码: https://github.com/Project-Robius-China/image-viewer-workshop

创建组件模块

首先,我们在项目源码 src 文件夹下,创建新的文件夹 components, 并且创建根文件 mod.rs:

1your-app/
2|—— resources
3├── Cargo.lock
4├── Cargo.toml
5├── src
6|   |—— components
7|   |     |—— mod.rs
8│   ├── app.rs
9│   ├── lib.rs
10│   └── main.rs

此时,整个组件模块需要被注册进 Makepad 的 live 系统中,在根组件 app.rs 中,将 app.rs 中的 LiveRegister 的实现代码改成:

1impl LiveRegister for App {
2    fn live_register(cx: &mut Cx) {
3        makepad_widgets::live_design(cx);
4        crate::components::live_design(cx);
5    }
6}

这样,我们定义的组件模块就已经成功注册进了 Makepad 项目中。自定义的组件,只需要在组件模块下完成注册,就可以在项目中直接使用。

创建组件

在之前的内容中,我们知道整个应用定义了三个组件:ImageItem, ImageRow, ImageGrid。所有这里我们创建组件的目的就是将这三个组件从 app.rs 中抽离出来,做成单独的组件。

创建 ImageItem 组件

在原 app.rs 下,ImageItem 定义如下:

1ImageItem = <View> {
2        width: 256,
3        height: 256,
4
5        image = <Image> {
6            width: Fill,
7            height: Fill,
8            fit: Biggest,
9            source: (PLACEHOLDER)
10        }
11    }

现在我们要把 ImageItem 单独成一个独立的组件,在 components 文件夹下,创建一个新的文件,命名为 image_item.rs, 并将下面的代码,加入带新的文件中:

1use makepad_widgets::*;
2
3live_design! {
4    use link::widgets::*;
5
6    PLACEHOLDER = dep("crate://self/resources/Rust.jpg");
7
8    pub ImageItem = {{ImageItem}} {
9        width: 256,
10        height: 256,
11        image_index: 0,
12
13        image = <Image> {
14            width: Fill,
15            height: Fill,
16            fit: Biggest,
17            source: (PLACEHOLDER)
18        }
19    }
20}
21
22#[derive(Live, LiveHook, Widget)]
23pub struct ImageItem {
24    #[deref]
25    view: View,
26}
27
28impl Widget for ImageItem {
29    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
30        self.view.handle_event(cx, event, scope);
31    }
32
33    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
34        self.view.draw_walk(cx, scope, walk)
35    }
36}

在之前 第 4 步第 5 步 中已经详细讲解了关于组件定义的相关内容,这里只是把相关组件内容抽取到单独的模块,这里不加以赘述。

之后,在 mod.rs 文件中,注册模块:

1pub mod image_item;

同时,我们要把定义的这个 ImageItem 组件注册进入 Makepad 的 live 系统中,我们要在 mod.rs 文件下加入:

1use makepad_widgets::*;
2
3pub fn live_design(cx: &mut Cx) {
4    image_item::live_design(cx);
5}

这一步,将定义的 ImageItem 组件成功注册进入了 Makepad 项目中,我们可以在应用中引用并且使用该组件。

创建 ImageRow 和 ImageGrid 组件

同样的,在 components 文件夹下,创建一个新的文件,命名为 ImageRow.rs:

1use makepad_widgets::*;
2use crate::app::State;
3
4live_design! {
5    use link::widgets::*;
6    use crate::components::image_item::ImageItem;
7
8    pub ImageRow = {{ImageRow}} {
9        <PortalList> {
10            height: 256,
11            flow: Right,
12
13            ImageItem = <ImageItem> {}
14        }
15	}
16}
17
18#[derive(Live, LiveHook, Widget)]
19pub struct ImageRow {
20    #[deref]
21    view: View,
22}
23
24impl Widget for ImageRow {
25    fn draw_walk(
26        &mut self,
27        cx: &mut Cx2d,
28        scope: &mut Scope,
29        walk: Walk,
30    ) -> DrawStep {
31        while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
32            if let Some(mut list) = item.as_portal_list().borrow_mut() {
33                let state = scope.data.get_mut::<State>().unwrap();
34                let row_idx = *scope.props.get::<usize>().unwrap();
35
36                list.set_item_range(cx, 0, state.num_images_for_row(row_idx));
37                while let Some(item_idx) = list.next_visible_item(cx) {
38                    if item_idx >= state.num_images_for_row(row_idx) {
39                        continue;
40                    }
41
42                    let item_widget_id = live_id!(ImageItem);
43                    let item = list.item(cx, item_idx, item_widget_id);
44
45                    let absolute_image_idx = state.first_image_idx_for_row(row_idx) + item_idx;
46
47                    item.apply_over(cx, live!{ image_index: (absolute_image_idx) });
48
49                    let filtered_image_idx = state.filtered_image_idxs[absolute_image_idx];
50
51                    let image_path = &state.image_paths[filtered_image_idx];
52                    let image_view = item.image(id!(image));
53                    image_view.load_image_file_by_path_async(cx, &image_path).unwrap();
54
55                    item.draw_all(cx, &mut Scope::empty());
56                }
57            }
58        }
59        DrawStep::done()
60    }
61
62    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
63        self.view.handle_event(cx, event, scope)
64    }
65}

其中:

1use crate::components::image_item::ImageItem;

引入 ImageItem 组件,由于 ImageItem 完成了注册,可以在项目中直接引入。

同样的,在 components 文件夹下,创建一个新的文件,命名为 ImageGrid.rs:

1use makepad_widgets::*;
2use crate::app::State;
3
4
5live_design! {
6    use link::widgets::*;
7    use crate::components::image_row::ImageRow;
8
9    pub ImageGrid = {{ImageGrid}} {
10        <PortalList> {
11            flow: Down,
12            ImageRow = <ImageRow> {}
13        }
14    }
15}
16
17
18#[derive(Live, LiveHook, Widget)]
19pub struct ImageGrid {
20    #[deref]
21    view: View,
22}
23
24impl Widget for ImageGrid {
25    fn draw_walk(
26        &mut self,
27        cx: &mut Cx2d,
28        scope: &mut Scope,
29        walk: Walk,
30    ) -> DrawStep {
31        while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
32            if let Some(mut list) = item.as_portal_list().borrow_mut() {
33                let state = scope.data.get_mut::<State>().unwrap();
34
35                list.set_item_range(cx, 0, state.num_rows());
36                while let Some(row_idx) = list.next_visible_item(cx) {
37                    if row_idx >= state.num_rows() {
38                        continue;
39                    }
40
41                    let row = list.item(cx, row_idx, live_id!(ImageRow));
42                    let mut scope = Scope::with_data_props(state, &row_idx);
43                    row.draw_all(cx, &mut scope);
44                }
45            }
46        }
47        DrawStep::done()
48    }
49
50    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
51        self.view.handle_event(cx, event, scope)
52    }
53}

完成对组件 ImageGrid 的定义。最后在模块 mod.rs 下,声明注册这两个组件:

1use makepad_widgets::*;
2
3pub mod image_item;
4pub mod image_row;
5pub mod image_grid;
6
7pub fn live_design(cx: &mut Cx) {
8    image_item::live_design(cx);
9    image_row::live_design(cx);
10    image_grid::live_design(cx);
11}

修改根组件内容

由于我们将组件拆分到了组件模块中,在 app.rslive_design! 中,我们要将最终使用的组件引入:

1use crate::components::image_grid::ImageGrid;

其他内容我们没有修改,所以最终 app.rslive_design! 中:

1live_design! {
2    use link::widgets::*;
3    use link::theme::*;
4    use crate::components::image_grid::ImageGrid;
5
6    PLACEHOLDER = dep("crate://self/resources/Rust.jpg");
7    LEFT_ARROW = dep("crate://self/resources/left_arrow.svg");
8    RIGHT_ARROW = dep("crate://self/resources/right_arrow.svg");
9    LOOKING_GLASS = dep("crate://self/resources/looking_glass.svg");
10
11    AlertDialog = <Modal> {
12        width: Fill
13        height: Fill
14
15        bg_view: <View> {
16            width: Fill
17            height: Fill
18            show_bg: true
19            draw_bg: {
20                fn pixel(self) -> vec4 {
21                    return #00000080;
22                }
23            }
24        }
25
26        content: <View> {
27            width: Fit
28            height: Fit
29            padding: 20
30            flow: Down
31            align: {x: 0.5, y: 0.5}
32
33            draw_bg: {
34                color: #333
35            }
36
37            dialog = <RoundedView> {
38                width: 300
39                height: 150
40                align: {x: 0.5, y: 0.5}
41                draw_bg: {
42                    color: #333
43                    border_color: #555
44                    border_size: 1.0
45                    border_radius: 4.0
46                }
47                padding: 20
48
49
50                message = <Label> {
51                    width: Fill
52                    height: Fit
53                    align: {x: 0.5}
54                    margin: {bottom: 20}
55                    draw_text: {
56                        text_style: { font_size: 12.0 }
57                        color: #fff
58                    }
59                    text: "默认消息"
60                }
61            }
62        }
63    }
64
65    SearchBox = <View> {
66        width: 150,
67        height: Fit,
68        align: { y: 0.5 }
69        margin: { left: 75 }
70
71        <Icon> {
72            icon_walk: { width: 12.0 }
73            draw_icon: {
74                color: #8,
75                svg_file: (LOOKING_GLASS)
76            }
77        }
78
79        query = <TextInput> {
80            empty_text: "Search",
81            draw_text: {
82                text_style: { font_size: 10 },
83                color: #8
84            }
85        }
86    }
87
88    MenuBar = <View> {
89        width: Fill,
90        height: Fit,
91
92        <SearchBox> {}
93        <Filler> {}
94        slideshow_button = <Button> {
95            text: "Slideshow"
96        }
97    }
98
99    ImageBrowser = <View> {
100        flow: Down,
101
102        <MenuBar> {}
103        <ImageGrid> {}
104    }
105
106    SlideshowNavigateButton = <Button> {
107        width: 50,
108        height: Fill,
109        draw_bg: {
110            color: #fff0,
111            color_down: #fff2,
112        }
113        icon_walk: { width: 9 },
114        text: "",
115        grab_key_focus: false,
116    }
117
118    SlideshowOverlay = <View> {
119        height: Fill,
120        width: Fill,
121        cursor: Arrow,
122        capture_overload: true,
123
124        navigate_left = <SlideshowNavigateButton> {
125            draw_icon: { svg_file: (LEFT_ARROW) }
126        }
127        <Filler> {}
128        navigate_right = <SlideshowNavigateButton> {
129            draw_icon: { svg_file: (RIGHT_ARROW) }
130        }
131    }
132
133    Slideshow = <View> {
134        flow: Overlay,
135
136        image = <Image> {
137            width: Fill,
138            height: Fill,
139            fit: Biggest,
140            source: (PLACEHOLDER)
141        }
142        overlay = <SlideshowOverlay> {}
143    }
144
145    App = {{App}} {
146        placeholder: (PLACEHOLDER),
147
148        ui: <Root> {
149            <Window> {
150                body = <View> {
151                    flow: Overlay,
152
153                    page_flip = <PageFlip> {
154                        active_page: image_browser,
155
156                        image_browser = <ImageBrowser> {}
157                        slideshow = <Slideshow> {}
158                    }
159
160                    alert_dialog = <AlertDialog> {}
161                }
162            }
163        }
164    }
165}

检查到目前为止的进度

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

确保你当前在你的包目录下,然后运行:

1cargo run --release

如果一切正常,程序应该正常运行。

下一步

在这一步中,我们将 app.rs 中的组件进行拆分成了不同的模块。下一步,我们会为程序增加一个扩展,当在网格模式下点击图片时候,自动切换到 幻灯片放映 模式