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:
- The window has three widget:
Button
/Label
/TextInput
, and each button click increments the number in the input field.
- The window background is colorful and changes with mouse movement.
First, use cargo new simple to create a crate.
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
:
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:
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.
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.
- 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.
- The
LiveRegister
trait is used to register App
with the Live system.
- 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:
- User Button Click:
- Operating system captures the click event
- Event Propagation to Application:
- Operating system passes the event to the Makepad application
- 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
- UI Tree Event Handling:
- Event is passed to the UI tree through
self.ui.handle_event(cx, event, &mut Scope::empty())
- Button Event Handling:
- Event reaches the Button component's
handle_event
method
- 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
- Action Collection:
- The Action is immediately added to Cx's internal action buffer (
ActionsBuf
)
- Event Handling Completion:
- Button and other components complete event handling
- Current Event Loop Ends:
handle_event
method execution completes
- Action Processing:
- In
self.match_event(cx, event)
, the event
matches Event::Actions
type
- 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.