在昨天初见 Iced 框架时,我发现其具有相当强的表达能力,并准备分析其复杂输入处理、复杂渲染的机制,并继续探索其他示例。
输入处理
注意这个函数签名:
1 2 3 4 5 6 7
| fn update( &self, interaction: &mut Interaction, event: Event, bounds: Rectangle, cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) {
|
这个函数把输入的事件、光标信息和自己的信息映射成对 event 的处理状态和可能出现的 Message,只是加了一个可变的状态,让这个函数有了一种有限状态机的感觉,这里利用 &mut 保证了其自身状态的线性。
可变的状态定义如下:
1 2 3 4 5 6
| pub enum Interaction { None, Drawing, Erasing, Panning { translation: Vector, start: Point }, }
|
这时候再看对鼠标输入的处理就很直观了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| mouse::Event::ButtonPressed(button) => { let message = match button { mouse::Button::Left => {
*interaction = if is_populated { Interaction::Erasing } else { Interaction::Drawing };
populate.or(unpopulate) } mouse::Button::Right => {
*interaction = Interaction::Panning { translation: self.translation, start: cursor_position, };
None } _ => None, };
(event::Status::Captured, message) } mouse::Event::CursorMoved { .. } => { let message = match *interaction {
Interaction::Drawing => populate, Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { Some(Message::Translated( translation + (cursor_position - start) * (1.0 / self.scaling), )) } _ => None, };
let event_status = match interaction { Interaction::None => event::Status::Ignored, _ => event::Status::Captured, };
(event_status, message) }
|
其状态也被用于确定鼠标光标的形状:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fn mouse_interaction( &self, interaction: &Interaction, bounds: Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { match interaction { Interaction::Drawing => mouse::Interaction::Crosshair, Interaction::Erasing => mouse::Interaction::Crosshair, Interaction::Panning { .. } => mouse::Interaction::Grabbing, Interaction::None if cursor.is_over(bounds) => { mouse::Interaction::Crosshair } _ => mouse::Interaction::default(), } }
|
渲染机制
首先看看 draw 的函数签名:
1 2 3 4 5 6 7 8
| fn draw( &self, _interaction: &Interaction, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec<Geometry> {
|
接受当前状态、渲染器类型(其实我觉得 renderer 不应该需要我们传)、当前主题以及自身信息的不可变借用,以及边界和光标信息,返回要绘制的内容。根据是否需要缓存有两种绘制思路,缓存式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let life = self.life_cache.draw(renderer, bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B));
frame.with_save(|frame| { frame.translate(center); frame.scale(self.scaling); frame.translate(self.translation); frame.scale(Cell::SIZE as f32);
let region = self.visible_region(frame.size());
for cell in region.cull(self.state.cells()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, Color::WHITE, ); } }); });
|
注意使用了一个 self.life_cache.draw,但实际上就是一个限定只能查 bounds 的 useMemo,我觉得这个设计并不好。还有非缓存式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let mut frame = Frame::new(renderer, bounds.size());
let hovered_cell = cursor.position_in(bounds).map(|position| { Cell::at(self.project(position, frame.size())) });
if let Some(cell) = hovered_cell { frame.with_save(|frame| { frame.translate(center); frame.scale(self.scaling); frame.translate(self.translation); frame.scale(Cell::SIZE as f32);
frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, Color { a: 0.5, ..Color::BLACK }, ); }); }
|
可以看到非缓存式就是每次直接自己实例化一个 Frame,缓存式则先定义一个 cache,然后去调用其 draw 方法。
在具体渲染的时候有微妙的叠加顺序,先 scale 再 translate 和反过来是不一样的,但这就涉及一些傻逼数学了。这个框架没能对这种数学建模,我略有失望。
时序逻辑
Iced 里面有一个专用的用于注册随时间产生的状态的函数:
1 2 3 4 5 6 7 8
| fn subscription(&self) -> Subscription<Message> { if self.is_playing { time::every(Duration::from_millis(1000 / self.speed as u64)) .map(Message::Tick) } else { Subscription::none() } }
|
虽然看上去有些多余,完全可以自己使用线程来写,但这实际上是一层封装:封装后可以直接描述「状态」本身,例如「每多少秒产生一次这个信息」,而不是直接每多少秒去产生这个信息,这种封装是可取的。注册之后就会连续地产生 Tick 事件。
最终逻辑被传到 Life 中,这里用 Life 表示「活着的」格子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| fn tick(&mut self) { let mut adjacent_life = FxHashMap::default();
for cell in &self.cells { let _ = adjacent_life.entry(*cell).or_insert(0);
for neighbor in Cell::neighbors(*cell) { let amount = adjacent_life.entry(neighbor).or_insert(0);
*amount += 1; } }
for (cell, amount) in adjacent_life.iter() { match amount { 2 => {} 3 => { let _ = self.cells.insert(*cell); } _ => { let _ = self.cells.remove(cell); } } } }
|
可以非常清晰地看到康威生命游戏的规则:格子周围有两个活着就维持自身状态,三个就变活,其他数量就挂掉。在 Iced 框架下可以非常清晰地把逻辑和 UI 框架解耦。UI 渲染结果只是核心数据结构的一个「镜像」,通过不可变引用的方式被固定的函数计算出来。