Rendering Mechanism
Drawing Process Control
DrawStep State Machine
1pub trait DrawStepApi {
2 fn done() -> DrawStep {
3 Result::Ok(())
4 }
5 fn make_step_here(arg: WidgetRef) -> DrawStep {
6 Result::Err(arg)
7 }
8 fn make_step() -> DrawStep {
9 Result::Err(WidgetRef::empty())
10 }
11 fn is_done(&self) -> bool;
12 fn is_step(&self) -> bool;
13 fn step(self) -> Option<WidgetRef>;
14}
15
16impl DrawStepApi for DrawStep {
17 fn is_done(&self) -> bool {
18 match *self {
19 Result::Ok(_) => true,
20 Result::Err(_) => false,
21 }
22 }
23 fn is_step(&self) -> bool {
24 match *self {
25 Result::Ok(_) => false,
26 Result::Err(_) => true,
27 }
28 }
29
30 fn step(self) -> Option<WidgetRef> {
31 match self {
32 Result::Ok(_) => None,
33 Result::Err(nd) => Some(nd),
34 }
35 }
36}
37
38pub type DrawStep = Result<(), WidgetRef>;
Advantages of designing a state machine based on Result
:
1.Supports Incremental Rendering
Result::Err(WidgetRef)
indicates rendering is incomplete, needs next step
Result::Ok(())
indicates rendering is complete
- This design allows rendering process to be divided into multiple executable steps
2.State Preservation
- Using
Result
can maintain state between rendering steps
Err(WidgetRef)
carries information about which component needs rendering next
3.Control Flow Management
- Elegantly handles rendering process using
?
operator
- Easy to implement rendering pause, continue, and termination
- Easy to optimize, supports complex interactions
4.Memory Efficiency
Result
is a zero-cost abstraction
- State machine transitions produce no additional overhead
Drawing Process
Let's explain the drawing process through a simple Button
widget:
1pub struct Button {
2 // Drawing state machine
3 #[rust] draw_state: DrawStateWrap<DrawState>,
4 // Layout information
5 #[layout] layout: Layout,
6 // Positioning information
7 #[walk] walk: Walk,
8 // Background drawing
9 #[live] draw_bg: DrawColor,
10 // Text drawing
11 #[live] draw_text: DrawText,
12}
13
14impl Widget for Button {
15 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
16 // 1. Initialize drawing state
17 if self.draw_state.begin(cx, DrawState::DrawBackground) {
18 // Start layout calculation
19 self.draw_bg.begin(cx, walk, self.layout);
20 return DrawStep::make_step();
21 }
22
23 // 2. Draw background
24 if let Some(DrawState::DrawBackground) = self.draw_state.get() {
25 self.draw_bg.end(cx);
26 // Switch to text drawing state
27 self.draw_state.set(DrawState::DrawText);
28 return DrawStep::make_step();
29 }
30
31 // 3. Draw text
32 if let Some(DrawState::DrawText) = self.draw_state.get() {
33 let text_walk = Walk::size(Size::Fill, Size::Fit);
34 self.draw_text.draw_walk(cx, scope, text_walk)?;
35 self.draw_state.end();
36 }
37
38 DrawStep::done()
39 }
40}
41
42// Drawing state enumeration
43#[derive(Clone)]
44enum DrawState {
45 DrawBackground,
46 DrawText
47}
Key points analysis:
- State Management
- DrawStateWrap manages drawing state
- Each state corresponds to a drawing phase
- Can interrupt and resume at any stage
- Layout System
1// Set layout
2self.draw_bg.begin(cx, walk, self.layout);
3
4// Turtle automatically handles:
5// - Element position calculation
6// - Margin/Padding handling
7// - Child element layout
- Progressive Drawing
1// Return continue marker
2return DrawStep::make_step();
3
4// Return completion marker
5return DrawStep::done();
- State Transitions
1// Switch to next state
2self.draw_state.set(DrawState::DrawText);
3
4// End drawing
5self.draw_state.end();
Drawing Model
Makepad employs a sophisticated but efficient deferred drawing system.
The core feature of this system is that drawing commands are not executed immediately, but are collected into a draw list (DrawList) for batch processing.
DrawList is the core of Makepad's drawing system. Let's look at its structure:
1// In platform/src/draw_list.rs
2pub struct CxDrawList {
3 pub debug_id: LiveId,
4 pub codeflow_parent_id: Option<DrawListId>,
5 pub redraw_id: u64,
6 pub pass_id: Option<PassId>,
7 pub draw_items: CxDrawItems,
8 pub draw_list_uniforms: CxDrawListUniforms,
9 pub rect_areas: Vec<CxRectArea>,
10}
11
12pub struct CxDrawItems {
13 pub(crate) buffer: Vec<CxDrawItem>,
14 used: usize
15}
16
17pub struct CxDrawItem {
18 pub redraw_id: u64,
19 pub kind: CxDrawKind,
20 pub draw_item_id: usize,
21 pub instances: Option<Vec<f32>>,
22 pub os: CxOsDrawCall
23}
When we call a redraw
command, here's what actually happens:
1impl DrawQuad {
2 pub fn draw(&mut self, cx: &mut Cx2d) {
3 // 1. Not immediate drawing, but collecting draw commands
4 if let Some(mi) = &mut self.many_instances {
5 // Batch processing mode: Add instance data to buffer
6 // This allows multiple similar draw operations to be batch processed,
7 // greatly reducing GPU calls
8 mi.instances.extend_from_slice(self.draw_vars.as_slice());
9 }
10 else if self.draw_vars.can_instance() {
11 // Single instance mode: Create new instance
12 let new_area = cx.add_aligned_instance(&self.draw_vars);
13 self.draw_vars.area = cx.update_area_refs(self.draw_vars.area, new_area);
14 }
15 }
16}
Drawing command merging:
1impl CxDrawList {
2 pub fn find_appendable_drawcall(
3 &mut self,
4 sh: &CxDrawShader,
5 draw_vars: &DrawVars
6 ) -> Option<usize> {
7 // Try to find mergeable draw calls
8 if let Some((_,draw_call)) = self.draw_items.iter_mut()
9 .find(|item| item.can_append(sh, draw_vars)) {
10 return Some(draw_call);
11 }
12 None
13 }
14}
View optimization:
1enum ViewOptimize {
2 None,
3 DrawList, // Use independent draw list
4 Texture // Render to texture cache
5}
For example, Makepad's built-in View
Widget uses ViewOptimize
optimization.
1. DrawList mode creates an independent draw list for the view that can be cached and reused.
Suitable scenarios:
- Interfaces with moderate frequency of changes
- Complex views that need to maintain interaction responsiveness
- Containers with many child elements
2. Texture mode renders the entire view to a texture, then uses this texture as a whole.
Suitable scenarios:
- Static or rarely changing content
- Views with complex visual effects but relatively stable content
- Interfaces requiring special visual effects (like blur, transforms, etc.)
In practice, view hierarchy can be reasonably divided.
1// Recommended view hierarchy structure
2RootView (No Optimization)
3├── StaticBackground (Texture)
4├── ContentArea (DrawList)
5│ ├── StaticWidgets (Texture)
6│ └── DynamicWidgets (DrawList)
7└── OverlayLayer (No Optimization)
Area Management
In Makepad, the core of area management is the Area
type, which is used to track and manage the drawing area of widgets on screen.
Each Widget has an associated Area, which is used not only for determining drawing position but also for event handling (Event) and hit detection.
1// Core area type definition
2#[derive(Clone, Copy, Debug)]
3pub enum Area {
4 Empty,
5 Instance(InstanceArea), // Instance area (for rendering instances)
6 Rect(RectArea) // Rectangle area (for basic shapes)
7}
8
9pub struct RectArea {
10 pub draw_list_id: DrawListId,
11 pub redraw_id: u64,
12 pub rect_id: usize
13}
14
15pub struct InstanceArea {
16 pub draw_list_id: DrawListId,
17 pub draw_item_id: usize,
18 pub instance_count: usize,
19 pub instance_offset: usize,
20 pub redraw_id: u64
21}
The lifecycle of an Area
can be visualized as follows:
Here's the core implementation of Area
management:
- Area Creation and Allocation:
1impl Widget {
2 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
3 // Start with layout calculation
4 if self.draw_state.begin(cx, DrawState::Begin) {
5 // Begin layout calculation
6 cx.begin_turtle(walk, self.layout);
7
8 // Area allocation
9 cx.walk_turtle_with_area(&mut self.area, walk);
10
11 // Area update and reference management
12 self.area = cx.update_area_refs(self.area, new_area);
13 }
14 }
15}
- Area Update and Tracking:
1impl Cx2d {
2 pub fn update_area_refs(&mut self, old_area: Area, new_area: Area) -> Area {
3 if old_area == Area::Empty {
4 return new_area;
5 }
6
7 // Update IME area
8 if self.ime_area == old_area {
9 self.ime_area = new_area;
10 }
11
12 // Update finger event areas
13 self.fingers.update_area(old_area, new_area);
14
15 // Update drag-drop areas
16 self.drag_drop.update_area(old_area, new_area);
17
18 // Update keyboard focus areas
19 self.keyboard.update_area(old_area, new_area);
20
21 new_area
22 }
23}
- Area Clipping, which controls content visibility, maintains visual boundaries, and handles content overflow.
- For Instance areas: Clipping information is stored in shader draw call uniforms
- For Rectangle areas: Clipping information is stored directly in the RectArea structure:
1pub struct CxRectArea {
2 pub rect: Rect, // The rectangle itself
3 pub draw_clip: (DVec2, DVec2) // Stores clipping range's min and max points
4}
The core clipping functionality is implemented in the clipped_rect()
method within the Area
source code.
For Instance areas:
- Clipping boundaries are stored in shader uniforms as four values (minX, minY, maxX, maxY)
- During drawing, the shader applies these clipping boundaries to limit pixel drawing range
- Clipping is implemented at the vertex shader stage by limiting vertex positions
For Rectangle areas:
- Clipping boundaries are stored directly in the
RectArea
structure
- Clipping is implemented through intersection of rectangles with their clipping boundaries
- This generates a new rectangle representing the visible portion
Event Handling
Makepad's event system is divided into several levels:
1// Top-level event enumeration
2pub enum Event {
3 FingerDown(FingerDownEvent),
4 FingerUp(FingerUpEvent),
5 FingerMove(FingerMoveEvent),
6 KeyDown(KeyEvent),
7 KeyUp(KeyEvent),
8 // ...
9}
10
11// Event hit detection results
12pub enum Hit {
13 KeyFocus(KeyFocusEvent),
14 FingerDown(FingerDownEvent),
15 Nothing
16}
Events are dispatched following this flow:
1impl Widget for MyWidget {
2 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
3 // Check if event hits this widget's area
4 match event.hits(cx, self.area()) {
5 Hit::FingerDown(e) => {
6 // Handle click event
7 cx.widget_action(uid, &scope.path, MyAction::Clicked);
8 }
9 Hit::KeyDown(e) => {
10 // Handle keyboard event
11 }
12 _ => ()
13 }
14 }
15}
Here's an example of a simple Button
event handling:
1#[derive(Live)]
2pub struct Button {
3 #[rust] pub area: Area,
4 #[live] pub text: String,
5 #[animator] pub animator: Animator,
6}
7
8impl Widget for Button {
9 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
10 let uid = self.widget_uid();
11
12 // Handle animation events
13 if self.animator_handle_event(cx, event).must_redraw() {
14 self.redraw(cx);
15 }
16
17 match event.hits(cx, self.area()) {
18 Hit::FingerDown(_) => {
19 // Trigger click animation
20 self.animator_play(cx, id!(down.on));
21 // Send click event action
22 cx.widget_action(uid, &scope.path, ButtonAction::Clicked);
23 }
24 Hit::FingerUp(_) => {
25 self.animator_play(cx, id!(down.off));
26 }
27 _ => ()
28 }
29 }
30}
Event System
The event system has a layered architecture:
- Low-level event system (Event) - Handles system and UI basic events
- Mid-level action system (Action) - Handles component communication and state updates
- High-level message system (Signal/Channel) - Handles cross-thread communication
Makepad's Thread Model
Makepad 分为主 UI 线程
和其他多个 Worker 线程
。
- Single UI Thread
- UI rendering and event handling occur on the main thread
- Main thread runs the event loop (event_loop)
- All UI updates must happen on the main thread
- Multiple Worker Threads
- Background tasks are managed through thread pools
- Multiple thread pool implementations available for different needs
- Worker threads don't directly manipulate UI
- Inter-thread Communication Mechanisms
- Action system for thread message passing
- Signal mechanism for thread synchronization
- Channel for data transfer
Main thread (UI thread) model:
1// Main thread entry point defined in app_main macro
2pub fn app_main() {
3 // Create Cx
4 let mut cx = Rc::new(RefCell::new(Cx::new(Box::new(move |cx, event| {
5 // Main event handling loop
6 if let Event::Startup = event {
7 *app.borrow_mut() = Some($app::new_main(cx));
8 }
9 if let Event::LiveEdit = event {
10 app.borrow_mut().update_main(cx);
11 }
12 app.borrow_mut().as_mut().unwrap().handle_event(cx, event);
13 }))));
14
15 // Register components, initialize etc.
16 $app::register_main_module(&mut *cx.borrow_mut());
17 live_design(&mut *cx.borrow_mut());
18 cx.borrow_mut().init_cx_os();
19
20 // Start event loop
21 Cx::event_loop(cx);
22}
Inter-thread communication mechanisms:
1// Global Action send channel
2static ACTION_SENDER_GLOBAL: Mutex<Option<Sender<ActionSendSync>>> = Mutex::new(None);
3
4// UI signal mechanism
5pub struct SignalToUI(Arc<AtomicBool>);
6
7// Thread communication Receiver/Sender
8pub struct ToUIReceiver<T> {
9 sender: Sender<T>,
10 pub receiver: Receiver<T>,
11}
12
13pub struct ToUISender<T> {
14 sender: Sender<T>,
15}
Thread pools included:
1// Standard thread pool for simple task execution
2pub struct RevThreadPool {
3 tasks: Arc<Mutex<Vec<Box<dyn FnOnce() + Send + 'static>>>>,
4}
5
6// Tagged thread pool for tasks that need categorization and cancellation
7pub struct TagThreadPool<T: Clone + Send + 'static + PartialEq> {
8 tasks: Arc<Mutex<Vec<(T, Box<dyn FnOnce(T) + Send + 'static>)>>>,
9}
10
11// Message thread pool for continuous inter-thread communication
12pub struct MessageThreadPool<T: Clone + Send + 'static> {
13 sender: Sender<Box<dyn FnOnce(Option<T>) + Send + 'static>>,
14 msg_senders: Vec<Sender<T>>,
15}
Main communication flow:
1// 1. Worker thread sends Action to main thread
2Cx::post_action(action); // Send via global ACTION_SENDER
3
4// 2. Main thread processes received Actions
5impl Cx {
6 pub fn handle_action_receiver(&mut self) {
7 while let Ok(action) = self.action_receiver.try_recv() {
8 self.new_actions.push(action);
9 }
10 self.handle_actions();
11 }
12}
13
14// 3. UI state update notification
15SignalToUI::set_ui_signal(); // Notify UI that update is needed
Event System Overview
Makepad provides an Event
mechanism for bottom-up propagation (distributed from system/framework to components) of system-level events (like mouse, keyboard, touch, etc.).
Events are synchronously processed global events.
1pub enum Event {
2 // Application lifecycle events
3 Startup,
4 Shutdown,
5 Foreground,
6 Background,
7 Resume,
8 Pause,
9
10 // UI interaction events
11 Draw(DrawEvent),
12 MouseDown(MouseDownEvent),
13 MouseMove(MouseMoveEvent),
14 KeyDown(KeyEvent),
15 TextInput(TextInputEvent),
16
17 // Custom events
18 Signal, // For inter-thread communication
19 Actions(ActionsBuf), // Container for custom actions
20 Timer(TimerEvent), // Timer events
21}
Additionally, Makepad provides an Action
mechanism for top-down propagation (sent from components to parent components/listeners) of internal business actions.
These Actions can be either synchronous or asynchronous.
Summary of Event and Action differences:
- Events are system-level input events, propagating bottom-up to transmit low-level events.
- Actions are component-level business actions, propagating top-down to transmit business actions.
1// Action trait definition
2pub trait ActionTrait: 'static {
3 fn debug_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
4 fn ref_cast_type_id(&self) -> TypeId;
5}
6
7// Concrete Action example, defining Button's business code
8#[derive(Clone, Debug)]
9pub enum ButtonAction {
10 Clicked,
11 Pressed,
12 Released
13}
Makepad provides a unified Action sending and handling mechanism through widget_action
, and Actions can carry data and state.
1// Action wrapper structure
2pub struct WidgetAction {
3 pub action: Box<dyn WidgetActionTrait>,
4 pub data: Option<Arc<dyn ActionTrait>>, // Associated data
5 pub widgets: SmallVec<[WidgetRef;4]>, // Widget references sending the action
6 pub widget_uid: WidgetUid, // Widget unique ID
7 pub path: HeapLiveIdPath, // Widget path
8 pub group: Option<WidgetActionGroup> // Group information
9}
10
11// Action group information
12pub struct WidgetActionGroup {
13 pub group_uid: WidgetUid,
14 pub item_uid: WidgetUid,
15}
Components send Actions through widget_action
:
1impl WidgetActionCxExt for Cx {
2 // Send a simple action
3 fn widget_action(
4 &mut self,
5 widget_uid: WidgetUid, // Widget ID
6 path: &HeapLiveIdPath, // Widget path
7 t: impl WidgetActionTrait // Action content
8 ) {
9 self.action(WidgetAction {
10 widget_uid,
11 data: None,
12 path: path.clone(),
13 widgets: Default::default(),
14 action: Box::new(t),
15 group: None,
16 })
17 }
18
19 // Send action with data
20 fn widget_action_with_data(
21 &mut self,
22 action_data: &WidgetActionData,
23 widget_uid: WidgetUid,
24 path: &HeapLiveIdPath,
25 t: impl WidgetActionTrait,
26 ) {
27 self.action(WidgetAction {
28 widget_uid,
29 data: action_data.clone_data(),
30 path: path.clone(),
31 widgets: Default::default(),
32 action: Box::new(t),
33 group: None,
34 })
35 }
36}
37
38#[derive(Default)]
39pub struct WidgetActionData{
40 data: Option<Arc<dyn ActionTrait>>
41}
Actions are collected in the context
's action buffer
:
1impl Cx {
2 pub fn action(&mut self, action: impl ActionTrait) {
3 self.new_actions.push(Box::new(action));
4 }
5}
Receivers get all Actions:
1// Capture all actions produced during an event handling process
2let actions = cx.capture_actions(|cx| {
3 self.button.handle_event(cx, event, scope);
4});
Then search for specific Actions:
1impl WidgetActionsApi for Actions {
2 // Find action by component path
3 fn widget_action(&self, path: &[LiveId]) -> Option<&WidgetAction> {
4 for action in self {
5 if let Some(action) = action.downcast_ref::<WidgetAction>() {
6 let mut ap = action.path.data.iter().rev();
7 if path.iter().rev().all(|p| ap.find(|&ap| p == ap).is_some()) {
8 return Some(action)
9 }
10 }
11 }
12 None
13 }
14
15 // Find action by component ID
16 fn find_widget_action(&self, widget_uid: WidgetUid) -> Option<&WidgetAction> {
17 for action in self {
18 if let Some(action) = action.downcast_ref::<WidgetAction>() {
19 if action.widget_uid == widget_uid {
20 return Some(action);
21 }
22 }
23 }
24 None
25 }
26}
Action type conversion and handling:
1// Example: Handle button click event
2impl ButtonRef {
3 pub fn clicked(&self, actions: &Actions) -> bool {
4 if let ButtonAction::Clicked = actions.find_widget_action(self.widget_uid()).cast() {
5 return true
6 }
7 false
8 }
9}
10
11// Usage example
12let actions = cx.capture_actions(|cx| {
13 self.button.handle_event(cx, event, scope);
14});
15
16if self.button.clicked(&actions) {
17 // Handle click event
18}
This mechanism allows Makepad's components to flexibly pass states and communicate events while maintaining good decoupling and maintainability.
Event Handling Process
Since the Widget
trait's handle_event
mainly focuses on two aspects:
1impl Widget for MyWidget {
2 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
3 // 1. Handle hit test events within area (clicks, drags, etc.)
4 match event.hits(cx, self.area()) {
5 Hit::FingerDown(e) => { ... }
6 Hit::KeyDown(e) => { ... }
7 }
8
9 // 2. Handle animation-related events
10 if self.animator_handle_event(cx, event).must_redraw() {
11 self.draw_key.area().redraw(cx)
12 }
13 }
14}
However, there are actually many events that are unrelated to Area, such as:
- Lifecycle events (startup, shutdown)
- Global events (foreground, background switches)
- Action handling
- Drawing events
- Animation frame updates
If all these events were handled in each Widget, the match event branches would be very redundant.
1// Without using MatchEvent
2impl Widget for MyWidget {
3 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
4 match event {
5 Event::Startup => { ... }
6 Event::Draw(e) => { ... }
7 Event::NextFrame(e) => { ... }
8 Event::Actions(e) => { ... }
9 // Still need to handle hit testing
10 _ => match event.hits(cx, self.area()) {
11 Hit::FingerDown(e) => { ... }
12 }
13 }
14 }
15}
Therefore, Makepad provides the MatchEvent
trait, which provides a series of default implementations to make the code clearer:
1#[derive(Default)]
2struct MyComplexWidget {
3 area: Area,
4 value: f64,
5 animator: Animator
6}
7
8// Widget trait handles core interaction logic
9impl Widget for MyComplexWidget {
10 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
11 let uid = self.widget_uid();
12
13 // 1. Animation handling
14 if self.animator_handle_event(cx, event).must_redraw() {
15 self.redraw(cx);
16 }
17
18 // 2. Interaction event handling
19 match event.hits(cx, self.area) {
20 Hit::FingerDown(_) => {
21 self.animator_play(cx, id!(down.on));
22 cx.widget_action(uid, &scope.path, MyAction::Clicked);
23 }
24 Hit::KeyDown(ke) => {
25 // Keyboard event handling
26 }
27 _ => ()
28 }
29
30 // 3. Use MatchEvent to handle other events
31 self.match_event(cx, event);
32 }
33}
34
35// MatchEvent trait handles business logic
36impl MatchEvent for MyComplexWidget {
37 // Lifecycle events
38 fn handle_startup(&mut self, cx: &mut Cx) {
39 // Initialize configuration
40 }
41
42 // State updates
43 fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
44 for action in actions {
45 if let MyAction::ValueChanged(new_value) = action.cast() {
46 self.value = new_value;
47 self.redraw(cx);
48 }
49 }
50 }
51
52 // Drawing related
53 fn handle_draw_2d(&mut self, cx: &mut Cx2d) {
54 // Custom drawing logic
55 }
56
57 // Animation frames
58 fn handle_next_frame(&mut self, cx: &mut Cx, e: &NextFrameEvent) {
59 // Animation updates
60 }
61}
This way components can focus on implementing their own event handling logic without writing lots of matching code.
Event handling priority in Makepad is as follows:
- Animation events (Animator)
- Direct interaction events (Hit)
- General system events (MatchEvent)
- Business Actions
Signal Mechanism
1// UI signal mechanism
2pub struct SignalToUI(Arc<AtomicBool>);
3
4impl SignalToUI {
5 // Set UI signal
6 pub fn set_ui_signal() {
7 UI_SIGNAL.store(true, Ordering::SeqCst)
8 }
9
10 // Check and clear signal
11 pub fn check_and_clear(&self) -> bool {
12 self.0.swap(false, Ordering::SeqCst)
13 }
14}
15
16// UI message channel
17pub struct ToUIReceiver<T> {
18 sender: Sender<T>,
19 pub receiver: Receiver<T>,
20}
21
22pub struct ToUISender<T> {
23 sender: Sender<T>,
24}