在上一步中,我们实现了一个模态窗,并成功的把模态窗的触发与关闭绑定到了对应的按钮/事件上。同时,我们的应用代码都在 app.rs
下,显得十分冗长,难以维护。在这一步中,我们将会将应用拆成几个不同的组件,并且放入不同的模块中。
注意: 如果你不想自己敲代码,可以在这里找到本步骤的完整代码: 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
中抽离出来,做成单独的组件。
在原 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 项目中,我们可以在应用中引用并且使用该组件。
同样的,在 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.rs
的 live_design!
中,我们要将最终使用的组件引入:
1use crate::components::image_grid::ImageGrid;
其他内容我们没有修改,所以最终 app.rs
的 live_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
中的组件进行拆分成了不同的模块。下一步,我们会为程序增加一个扩展,当在网格模式下点击图片时候,自动切换到 幻灯片放映 模式