Basic

Typically, an application consists of multiple components, including various windows, buttons, input fields, labels, and other widgets, or subcomponents composed of these widgets.

A Component, broadly speaking, is a modular unit that encapsulates specific functionality, typically containing both logic and view definitions.

Components can be composed of multiple Widgets or serve as subcomponents of other components. They usually have independent lifecycles.

Components have the following characteristics:

  • Modularity: Components are modular units for building user interfaces, making them easy to reuse and maintain.
  • Encapsulation: Components encapsulate their own logic and view, reducing coupling with other components.
  • Composability: Components can nest other components to build complex interfaces.

Makepad Widget, as part of the Makepad framework, includes preset basic UI widgets through the makepad-widgets library, such as buttons, labels, and input fields.

These widgets aim to simplify UI construction and management, enabling developers to quickly create complex UI components. Therefore, we need to develop a deep understanding of Makepad Widget.

Differences Between Widgets and Components

Widget:

  • Basic UI building blocks
  • Direct handling of events and drawing
  • Focus on fundamental UI capabilities

Component:

  • Compositions of Widgets
  • Higher-level abstractions
  • Focus on business logic
  • Combine existing Widgets
  • Greater reusability

Components with Declarative UI

Let's begin understanding these built-in widgets through a custom component example. This example comes from the simple example in Makepad, though we'll modify it slightly for this chapter.

The final effect is shown in the video:

  1. The window has three widget: Button/Label/TextInput, and each button click increments the number in the input field.
  2. The window background is colorful and changes with mouse movement.

First, use cargo new simple to create a crate.

1cargo new simple

Add the makepad-widgets library dependency in Cargo.toml

1[dependencies]
2# use rik branch,because it is the active development branch
3makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik" }

Then create lib.rs and app.rs files under src.

Your directory structure should look like this:

1simple/
2├── Cargo.lock
3├── Cargo.toml
4├── src
5│   ├── app.rs
6│   ├── lib.rs
7│   └── main.rs

Add the following code to lib.rs:

1pub mod app;

Then let's complete our code in the app.rs module. A basic App is a component - how should we define it?

Let's start by defining the overall component structure:

1use makepad_widgets::*; // Import Makepad Widgets package
2
3// Define live_design macro for declaring UI components and layout
4live_design! {
5	App = {{App}} {
6        // Define the root node of the UI tree
7        ui: <Root>{
8            // TODO
9        }
10	}
11}
12
13// Define application entry point
14app_main!(App);
15
16// Define App struct containing UI and counter
17#[derive(Live, LiveHook)]
18pub struct App {
19    #[live]
20    ui: WidgetRef, // UI component reference
21    #[rust]
22    counter: usize, // Counter
23}
24
25// Implement LiveRegister trait for registering live design
26impl LiveRegister for App {
27    fn live_register(cx: &mut Cx) {
28        // Register Makepad Widgets' live design
29        makepad_widgets::live_design(cx);
30    }
31}
32
33// Implement AppMain trait for handling events
34impl AppMain for App {
35    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
36        // Match and handle events
37        self.match_event(cx, event);
38        // Handle UI events
39        self.ui.handle_event(cx, event, &mut Scope::empty());
40    }
41}

Let's explain the code structure:

  1. live_design!,The live_design! macro is used to declare UI components and layout. It's part of the Live system we discussed earlier, implementing Live DSL using Rust macros to enable runtime UI modifications.
  2. app_main!(App)defines the application entry point. Since Makepad needs to support cross-platform applications (including Web/iOS/Android/MacOS/Windows/Linux), the app_main! macro internally includes entry point code for various platforms, hence using a macro rather than a simple main function.
  3. The Rust struct App uses the derive(Live, LiveHook) derive macro, automatically implementing two Live system-related traits for App: Live and LiveHook.
    • The ui field of type WidgetRef can be thought of as dyn Widget, representing UI controls that implement the Widget trait. These controls can be updated at runtime, marked with the #[live] attribute, which automatically implements certain derive macros, like creating LiveId for the field and calling LiveHook methods.
    • The counter field belongs to business logic, used for counting, so it's marked with the #[rust] attribute, telling the Live system it belongs to Rust's domain rather than UI controls and doesn't participate in runtime updates.
    • The Live and LiveHook traits are related to Makepad controls' Live lifecycle, which we'll discuss in detail later.
  4. The LiveRegister trait is used to register App with the Live system.
  5. The AppMain trait defines the handle_event method for passing various events to the UI tree, including mouse, timer, or scroll events.
    • This trait object is actually called in app_main!, making it part of the program entry point.
    • Internally, ui's handle_event takes parameters cx/event for context information and events, while Scope is used for component state sharing, with Scope::empty() currently representing no shared state.

Makepad is a declarative UI framework, meaning you only need to declare the desired UI style, and the framework will automatically update UI changes.

makepad makes it easier to build and manage UI by declaring UI styles inside live_design! macros using the Live DSL language in a declarative way.

1live_design! {
2    // Import base components and desktop theme
3    use link::theme::*;
4    use link::shaders::*;
5    use link::widgets::*;
6
7    // Define App component
8    App = {{App}} {
9        // Define the UI tree root node
10        ui: <Root>{
11            // Define main window
12            main_window = <Window>{
13                // Show background
14                show_bg: true
15                width: Fill,
16                height: Fill
17
18
19                // Define custom background drawing
20                draw_bg: {
21                    fn pixel(self) -> vec4 {
22                         // Get geometric position
23                        let st = vec2(
24                            self.geom_pos.x,
25                            self.geom_pos.y
26                        );
27
28                         // Calculate color based on x and y position and time
29                        let color = vec3(st.x, st.y, abs(sin(self.time)));
30                        return vec4(color, 1.0);
31                    }
32								}
33                // Define window body
34                body = <ScrollXYView>{
35                    // Vertical layout direction
36                    flow: Down,
37                    // 10-unit spacing between children
38                    spacing:10,
39                    // Alignment
40                    align: {
41                        x: 0.5,
42                        y: 0.5
43                    },
44                    // Button component
45                    button1 = <Button> {
46                        text: "Hello world"
47                        draw_text:{color:#f00}  // Red text color
48                    }
49
50
51                    // Text input component
52                    label1 = <Label> {
53                        draw_text: {
54                            color: #f // White text color
55                        },
56                        text: "Click to count "
57                    }
58
59                    // Text input component
60                    input1 = <TextInput> {
61                        width: 100, height: 30
62                        text: "Counter: 0 "
63                    }
64                }
65            }
66        }
67    }
68}

Basic Components and Theme

1use link::theme::*;
2use link::widgets::*;

These two use statements are used to import base components and the default theme. Note that the use keyword is specific to Live DSL, typically used to import Live scripts defined by live_design! in other modules.

Defining the App Component

1// Define App struct containing UI and counter
2#[derive(Live, LiveHook)]
3pub struct App {
4    #[live]
5    ui: WidgetRef, // UI component reference
6    #[rust]
7    counter: usize, // Counter
8}
9
10live_design! {
11    App = {{App}} {
12            // Define the UI tree root node
13        ui: <Root>{
14            ...
15        }
16    }
17}

The App component is defined in live_design! using the App = {{App}} syntax. This creates an App component instance, where the App inside {{App}} corresponds to the Rust-defined App struct.

The App struct has two fields: ui and counter. The ui field represents UI components, so it's annotated with the #[live] attribute, while counter is a non-UI field, so it's annotated with #[rust].

In the live_design! DSL macro, ui: <Root> { ... } indicates that the UI component uses a Root root node.

Root Component

The Root component plays a crucial role in Makepad, primarily responsible for managing and coordinating multiple child components.

1live_design!{
2    RootBase = {{Root}} {}
3}
4
5#[derive(Live, LiveRegisterWidget, WidgetRef)]
6pub struct Root {
7    // Track current drawing state
8    #[rust] draw_state: DrawStateWrap<DrawState>,
9    // This is a ComponentMap<LiveId, WidgetRef> for storing and managing multiple windows
10    // Each window is a Widget, identified by LiveId
11    #[rust] windows: ComponentMap<LiveId, WidgetRef>,
12}
13...

This component manages multiple windows through LiveId and WidgetRef, and can track the current drawing state.

Rendering Mechanism

Makepad Widget adopts a hybrid approach combining immediate mode and retained mode rendering, providing high flexibility and performance optimization.

  • Retained mode preserves the state and structure of UI elements, updating only when necessary, suitable for static or infrequently changing UI elements.
  • Immediate mode recalculates and draws UI elements every frame, providing greater flexibility and ease of implementing dynamic UI, suitable for frequently changing UI elements.

Makepad adopts a hybrid mode, maintaining the rendering UI tree's structural state, balancing performance and flexibility. It traverses all windows and components that need updating, recalculating and drawing immediate mode parts while only updating changed parts for retained mode components.

The Root component is an example of this hybrid approach between retained and immediate modes.

Window Component

1// Define App component
2App = {{App}} {
3    // Define the UI tree root node
4    ui: <Root>{
5        // Define main window
6        main_window = <Window>{
7            // Show background
8            show_bg: true
9            width: Fill,
10            height: Fill
11
12            // Define custom background drawing
13            draw_bg: {
14                fn pixel(self) -> vec4 {
15                    ...
16                }
17            }
18            // Define window body
19            body = <ScrollXYView>{
20                // Vertical layout direction
21                flow: Down,
22                // 10-unit spacing between children
23                spacing:10,
24                // Alignment
25                align: {
26                    x: 0.5,
27                    y: 0.5
28                },
29                // Button component
30                button1 = <Button> {
31                    text: "Hello world"
32                    draw_text:{color:#f00} // Red text color
33                }
34
35                // Label component
36                label1 = <Label> {
37                    draw_text: {
38                        color: #f // White text color
39                    },
40                    text: "Click to count "
41                }
42
43                // Text input component
44                input1 = <TextInput> {
45                    width: 100, height: 30
46                    text: "Counter: 0 "
47                }
48            }
49        }
50    }
51}

Inside the Root component, we define a main_window window component as a <Window> instance. Similarly, this <Window> component corresponds to the Window Rust struct defined in widgets/window.rs.

In the main_window instance, we declare some basic properties: show_bg / width / height, and a special property draw_bg. This property is special because it starts with the draw_ prefix and can be followed by a code block to specify shader scripts - here it specifies a fn pixel(self) -> vec4 function to render the background color.

Additionally, the main_window instance defines a body component instance, which corresponds to a scroll view component <ScrollXYView>. Similarly, this body instance defines some basic properties of the component and its contained component instances:

  • <Button>, button instance
  • <Label>, label instance
  • <TextInput>, text input instance

With this, our Simple UI declaration is complete. Next, we'll add specific business logic to this UI.

Adding Business Logic

Our Simple UI's business logic is: when clicking the button, the value in the text input box will change to display the incremented count, adding one with each button click.

1// Implement MatchEvent trait for handling events
2impl MatchEvent for App {
3    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
4        // Check if button was clicked
5        // We can directly use the button1 instance through `id!()` to get the clicked Button
6        // `clicked` is a method of the Button component
7        if self.ui.button(id!(button1)).clicked(&actions) {
8            // Increment counter
9            log!("BUTTON jk {}", self.counter);
10            self.counter += 1;
11
12            // Update label text
13            // Similarly, get the input1 text input instance through `id!()`
14            let input = self.ui.text_input(id!(input1));
15            // Use the text input component's built-in `set_text_and_redraw`
16            // to update the text input content with the new counter value and trigger immediate redraw
17            input.set_text_and_redraw(cx, &format!("Counter: {}", self.counter));
18        }
19    }
20}

The id!() macro obtains the LiveId of the passed component instance. In Makepad, the Live System, draw library, and Widget library are deeply coupled. Component rendering order depends on LiveId. We'll delve deeper into Live System-related content in subsequent chapters.

We implement the MatchEvent trait for App to implement our business logic. This trait is defined in makepad-draw/src/match_event.rs and serves as the core interface for handling various events in the Makepad framework.

The trait defines multiple event handling methods, such as handle_startup, handle_shutdown, handle_draw, etc. This abstracts various platform-level events into a unified interface, simplifying cross-platform development.

The handle_actions method is used to handle multiple actions, with the default implementation iterating through all actions and calling handle_action.

The MatchEvent trait also defines a default implementation of match_event, which calls the handle_actions method.

In our previously defined App entry event handling method handle_event, we called this match_event method:

1// Implement AppMain trait for handling events
2impl AppMain for App {
3    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
4        // Match and handle events
5        self.match_event(cx, event);
6        // Handle UI events, in this case `<Root>`
7        // This calls the handle_event method defined when implementing Widget trait
8        self.ui.handle_event(cx, event, &mut Scope::empty());
9    }
10}

This reflects Makepad's architectural decision to separate application logic and UI component concerns. Application-level event handling (match event) is typically higher-level and abstract. UI event handling focuses on specific component interactions.

Action

Action is an important concept for Makepad, used to represent user interface interactions or state changes.

The essence of Action is defined in platform/src/action.rs:

1pub trait ActionTrait: 'static  {
2    fn debug_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
3    fn ref_cast_type_id(&self) -> TypeId where Self: 'static {TypeId::of::<Self>()}
4}
5
6impl<T: 'static + Debug + ?Sized > ActionTrait for T {
7    fn debug_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{
8        self.fmt(f)
9    }
10}
11
12generate_any_trait_api!(ActionTrait);
13
14impl Debug for dyn ActionTrait{
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{
16        self.debug_fmt(f)
17    }
18}
19
20pub type Action = Box<dyn ActionTrait>;
21pub type ActionsBuf = Vec<Action>;
22pub type Actions = [Action];
23
24pub trait ActionCast<T> {
25    fn cast(&self) -> T;
26}
27
28impl Cx{
29    pub fn action(&mut self, action: impl ActionTrait){
30        self.new_actions.push(Box::new(action));
31    }
32
33    pub fn extend_actions(&mut self, actions: ActionsBuf){
34        self.new_actions.extend(actions);
35    }
36
37    pub fn map_actions<F, G, R>(&mut self, f: F, g:G) -> R where
38    F: FnOnce(&mut Cx) -> R,
39    G: FnOnce(&mut Cx, ActionsBuf)->ActionsBuf,
40    {
41        ...
42    }
43
44    pub fn capture_actions<F>(&mut self, f: F) -> ActionsBuf where
45    F: FnOnce(&mut Cx),
46    {
47        ...
48    }
49}

This code defines the core structure and behavior of Actions in the Makepad platform:

  • ActionTrait: Can be viewed as an extension or custom version of the Any trait, automatically implemented for all 'static types that implement the Debug trait. The ref_cast_type_id method provides functionality similar to Any::type_id.
  • Action: Actually a trait object of ActionTrait. Actions is an array of these trait objects, a dynamically sized type similar to [T], so it's used as &Actions.
  • Cx: A cross-platform context struct that abstracts various methods for operating on actions, hiding different platform-level implementations.

The widgets/src/widget.rs defines the core Widget trait interface to manage methods that must be implemented for Widgets. WidgetRef is an encapsulation of the dyn Widget trait object.

Additionally, there's the WidgetActionTrait-related trait, specifically designed for the Widget system as a specialization of ActionTrait. This allows Widgets to dynamically select and execute different WidgetActions at runtime.

1pub trait WidgetActionTrait: 'static {
2    fn ref_cast_type_id(&self) -> TypeId;
3    fn debug_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
4    fn box_clone(&self) -> Box<dyn WidgetActionTrait>;
5}
6
7impl<T: 'static + ? Sized + Clone + Debug> WidgetActionTrait for T {
8    fn ref_cast_type_id(&self) -> TypeId {
9        TypeId::of::<T>()
10    }
11
12    fn box_clone(&self) -> Box<dyn WidgetActionTrait> {
13        Box::new((*self).clone())
14    }
15    fn debug_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{
16        self.fmt(f)
17    }
18}
19
20impl Debug for dyn WidgetActionTrait{
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result{
22        self.debug_fmt(f)
23    }
24}
25
26generate_any_trait_api!(WidgetActionTrait);
27
28impl Clone for Box<dyn WidgetActionTrait> {
29    fn clone(&self) -> Box<dyn WidgetActionTrait> {
30        self.as_ref().box_clone()
31    }
32}
33
34#[derive(Clone, Debug)]
35pub struct WidgetAction {
36    action: Box<dyn WidgetActionTrait>,
37    pub widget_uid: WidgetUid,
38    pub path: HeapLiveIdPath,
39    pub group: Option<WidgetActionGroup>
40}
41
42
43impl WidgetActionsApi for Actions{
44    fn find_widget_action(&self, widget_uid: WidgetUid) -> Option<&WidgetAction>{
45        for action in self{
46            // Here action will be converted to WidgetAction
47            if let Some(action) = action.downcast_ref::<WidgetAction>(){
48                if action.widget_uid == widget_uid{
49                    return Some(action)
50                }
51            }
52        }
53        None
54    }
55}

Because WidgetAction also implements ActionTrait by default, the action can be converted to WidgetAction here.

Button Widget Implementation Mechanism

Let's examine how Makepad's built-in Button Widget is implemented:

1live_design! {
2    ButtonBase = {{Button}} {}
3}
4
5#[derive(Clone, Debug, DefaultNone)]
6pub enum ButtonAction {
7    None,
8    /// The button was pressed (a "down" event).
9    Pressed(KeyModifiers),
10    /// The button was clicked (an "up" event).
11    Clicked(KeyModifiers),
12    /// The button was released (an "up" event), but should not be considered clicked
13    /// because the mouse/finger was not over the button area when released.
14    Released(KeyModifiers),
15}
16
17#[derive(Live, LiveHook, Widget)]
18pub struct Button {
19    ......
20    // Various property fields defined
21}
22
23impl Widget for Button {
24     fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
25         let uid = self.widget_uid();
26         ...
27         if self.enabled && self.visible {
28            match event.hits(cx, self.draw_bg.area()) {
29                Hit::FingerDown(fe) => {
30                    if self.grab_key_focus {
31                        cx.set_key_focus(self.draw_bg.area());
32                    }
33                    cx.widget_action(uid, &scope.path, ButtonAction::Pressed(fe.modifiers));
34                    self.animator_play(cx, id!(hover.pressed));
35                }
36                ...
37              }
38              ...
39          }
40     }
41
42     fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
43        if !self.visible {
44            return DrawStep::done();
45        }
46
47        self.draw_bg.begin(cx, walk, self.layout);
48        self.draw_icon.draw_walk(cx, self.icon_walk);
49        self.draw_text
50            .draw_walk(cx, self.label_walk, Align::default(), self.text.as_ref());
51        self.draw_bg.end(cx);
52        DrawStep::done()
53    }
54}
55
56impl Button {
57    pub fn clicked_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
58        if let ButtonAction::Clicked(m) = actions.find_widget_action(self.widget_uid()).cast() {
59            Some(m)
60        } else {
61            None
62        }
63    }
64
65    ...
66}

Here we're only showing the Button Widget's code structure, not the complete code.

To implement a Widget, one must implement the Widget trait, including two required methods: handle_event and draw_walk.

The handle_event method matches specific events, such as Hit::FingerDown. When a match is successful, it generates a corresponding WidgetAction through cx.widget_action. This includes actions like ButtonAction::Pressed.

The draw_walk method is Makepad framework's core method for drawing Widgets. It combines layout calculation and actual drawing operations.

Action Flow Summary

When a button is clicked, here's the actual flow:

  1. User Button Click:
    • Operating system captures the click event
  2. Event Propagation to Application:
    • Operating system passes the event to the Makepad application
  3. Event Enters Main Loop (when application starts, app_main! initiates a main loop):
    • In the current event loop, the click event is passed to AppMain::handle_event
  4. UI Tree Event Handling:
    • Event is passed to the UI tree through self.ui.handle_event(cx, event, &mut Scope::empty())
  5. Button Event Handling:
    • Event reaches the Button component's handle_event method
  6. Action Generation:
    • Button recognizes the click, immediately generates ButtonAction::Pressed or ButtonAction::Clicked
    • Using cx.widget_action(uid, &scope.path, ButtonAction::Clicked(fe.modifiers)) will create a WidgetAction
  7. Action Collection:
    • The Action is immediately added to Cx's internal action buffer (ActionsBuf)
  8. Event Handling Completion:
    • Button and other components complete event handling
  9. Current Event Loop Ends:
    • handle_event method execution completes
  10. Action Processing:
    • In self.match_event(cx, event), the event matches Event::Actions type
  11. State Update and UI Refresh:
    • Based on processed Actions, update application state and trigger necessary UI updates

This flow demonstrates how Makepad converts low-level events into high-level abstract Actions and propagates them throughout the application. This design allows:

  • Distribution of event handling logic across various Widgets
  • Provision of a unified mechanism for handling user interactions
  • Implementation of complex interaction patterns, such as cross-component communication
  • Maintenance of type safety while allowing dynamic behavior

The elegance of this system lies in its combination of immediate event handling and deferred action processing, providing a balance between flexibility and performance.