Author: AlexZhang
Objective: Ensure Robrix code maintains clarity, efficiency, and maintainability at the architectural level, following best practices for Makepad and Rust.
This checklist focuses on architectural decisions rather than general coding standards, aiming to be concise to help developers grasp basic principles rather than constraining them with excessive rules.
Core principles summary:
Cx::post_action
, cx.widget_action
), thread-safe queues (SegQueue
), specific channels (mpsc
, watch
, crossbeam_channel
).AppState
, per-room TimelineUiState
, various caches.sliding_sync.rs
) contain or directly manage states that are only for UI display (e.g., "loading", "failed", "selected" flags, UI element visibility flags)?@room
string) to determine business logic (such as whether to set mentions.room
)?RoomsList
, RoomScreen
, RoomPreview
) clearly handle their own display logic, such as filtering, sorting, and formatting based on received data?AppState
, RoomsList::all_rooms
, various *_cache.rs
) separated from data display logic?MentionableTextInput
) fully encapsulate the intent parsing logic related to that input (e.g., distinguishing whether a user typed normal "@room" text or selected @room
mention from a suggestion list)?RoomScreen
)?MentionableTextInput
, EditingPane
) through the Scope::props
mechanism?MatrixRequest
variant take on too many different responsibilities? (For example, a function that both queries login types and performs login operations).draw_walk
):
draw_walk
method contain non-drawing logic, such as permission checks, complex calculations, state updates, or triggering Actions?draw_walk
should be as lightweight as possible, only drawing based on currently determined state. State updates should be done in handle_event
or handle_actions
.draw_walk
, handle_event
, handle_actions
avoid operations that might block the main thread? (For example, direct file I/O, network requests, long-running loops, frequent or long lock waits).submit_async_request
?HashMap
/BTreeMap
efficient? (For example, is the entry
API used to avoid duplicate lookups when insertion or update is needed?).clone()
calls, especially in loops or frequently called code paths? Could references (&
or &mut
) be passed instead?Vec
s followed by immediate iteration avoided?WidgetRef
extension methods) clear in intent, easy to understand and use?AvatarRow
API).MentionableTextInput
) provide clear APIs to get the final parsed result information (for example, a method that returns a Mentions
struct)?RoomScreen
) to re-parse text or query internal state.WidgetRef
extension functions and their internally called Widget
methods (pub
vs private) match and make sense?matrix_sdk::ruma::events::room::message::Mentions
) used to precisely represent intent (including user IDs and whether @room
is mentioned), rather than relying on boolean flags or string checks?Cx::post_action
+ SignalToUI
.cx.widget_action
.Arc<Mutex<T>>
or RwLock
) for direct data modification between different components to drive UI updates avoided?handle_event
/ handle_actions
are responsible for responding to events/actions and updating state.redraw()
to request redrawing.draw_walk
draws only based on current state, should not contain state modification or complex logic.live!{}
macro and apply_over
method used?#[derive(Clone, DefaultNone, Debug)]
and include a None
variant?[ ] Cross-Platform Compatibility Check: Robrix supports different layouts for desktop and mobile, requiring specific checks for cross-platform adaptation:
[ ] Enhanced Error Handling and User Experience Checks:
[ ] Security Checks
The appendix section can be continuously updated and enriched to better consolidate project knowledge.
This section aims to detail the core architectural principles involved in the Robrix Code Review Checklist, combined with Robrix's specific practices. By understanding and following these architectural principles integrated with Robrix's concrete practices, the team can more effectively develop code and conduct reviews, building more robust, efficient, and maintainable applications.
Diagram Description:
app.rs
, home/
, shared/
etc. are mainly responsible for UI display and interaction, running on the main thread. sliding_sync.rs
's async_worker
, async_main_loop
, timeline_subscriber_handler
, etc. run on the background Tokio thread, handling network, SDK interaction, etc. This is the most core separation.App
is responsible for top-level navigation and global modal dialogs.HomeScreen
is responsible for overall layout (desktop/mobile).RoomScreen
is responsible for the display and interaction logic of a single room.RoomsList
is responsible for the display and filtering of the room list.RoomsList
holds all_rooms
(data storage) and displayed_rooms
(display state), with filtering logic in RoomsList
, rather than having the backend return filtered data.timeline_subscriber_handler
) send raw or near-raw data updates (TimelineUpdate
), while RoomScreen
is responsible for interpreting these updates and modifying its TimelineUiState
, ultimately affecting rendering.sliding_sync.rs
.TimelineUpdate::PaginationRunning
, RoomScreen
receives it and sets the visibility of TopSpace
.draw_walk
.handle_event
or handle_actions
, and storing the result (such as whether a button is enabled) in Widget state, draw_walk
only reads that state.MentionableTextInput
directly parsing raw text to judge @room
.MentionableTextInput
parsing user intent, generating a Mentions
object, RoomScreen
using that object.Principle Explanation: Written code should not only be functionally correct but also perform well in terms of resource consumption (CPU, memory) and response time, especially on the UI thread which directly affects user experience.
Importance:
Robrix Manifestation and Practice:
draw_walk
, handle_event
, handle_actions
that might take more than a few milliseconds (network, file I/O, complex calculations, long-duration locks).submit_async_request
.RoomsList::all_rooms
) for every event or Action. Updates should be as precise as possible, only processing affected parts.retain
, filter
, map
, entry
API to handle collections and Maps.&
, &mut
, &Arc
), especially in loops and frequently called functions. Arc
cloning itself is cheap, but cloning its internal data (such as Vec
) is expensive.avatar_cache
, media_cache
, user_profile_cache
, room_member_manager
to store already fetched data, avoiding repeated requests.get_or_fetch_*
) should check if the cache exists, and if not present and not requested, initiate a background request and mark as Requested
, avoiding duplicate requests.draw_walk
Lightweight:
draw_walk
should only do drawing-related things, relying on already prepared state. All calculations, formatting (such as timestamps), data conversion should be done during state updates (handle_event
/actions
).Principle Explanation: When designing components and modules, provide clear, concise, easy-to-understand and use public interfaces (APIs), while hiding complex internal implementation details. Good encapsulation reduces coupling, improves code maintainability and reusability.
Importance:
Robrix Manifestation and Practice:
WidgetRef
Extension: Providing type-safe, object-oriented APIs by implementing extension Traits for WidgetRef
(such as AvatarWidgetRefExt
).AvatarRow::set_avatar_row
encapsulate the logic of setting data and updating internal state, better than exposing multiple methods that need to be called in a specific order (such as set_range
+ iter_mut
).#[rust]
state fields of parent components. They should interact through props
(read-only) or Actions (requesting modification).UserProfilePaneInfo
, CalloutTooltipOptions
) or meaningful tuples to encapsulate multiple return values. Use specialized types (such as Mentions
) to pass data with specific semantics.fn
rather than pub fn
).Principle Explanation: Effectively utilize the core mechanisms provided by the Makepad framework (event handling, Action system, draw loop, Live Design) to build applications, rather than trying to bypass or reinvent wheels.
Importance:
Robrix Manifestation and Practice:
Cx::post_action
-> SignalToUI
-> UI handle_actions
-> update state -> redraw()
. This is the standard process.cx.widget_action
-> Parent Widget handle_actions
-> update state -> redraw()
.Arc<Mutex<T>>
or RwLock
to directly share and modify state between UI components to drive updates, which is error-prone and may block UI. Actions are preferred.redraw()
. draw_walk
function is only responsible for reading current state and rendering.#[rust]
state in draw_walk
.live!{}
and apply_over
to dynamically modify attributes defined by Live Design (colors, margins, visibility, etc.) in Rust code.#[derive(Clone, DefaultNone, Debug)]
and including a None
variant.The Makepad framework adopts a unique hybrid paradigm, aimed at combining the speed and flexibility of declarative UI definition with the performance and type safety of the Rust language. Its core idea can be summarized as: Live Design DSL defines structure and style, Rust handles logic and state, communication through the Action mechanism, and follows state-driven rendering loop.
1. Core Philosophy: Live Design DSL + Rust Logic Separation
live_design!
):
Walk
, Layout
), visual style (Draw*
shaders), animation (animator
), and component default/configurable properties (#[live]
).#[derive(Live, LiveHook)]
macro to associate Rust structs (usually App
or custom Widget) with definitions in DSL. #[live]
attributes mark fields that can be set or overridden in DSL. LiveHook
trait (such as after_apply
, after_new_from_doc
) allows initialization or response logic to be executed after DSL updates are applied to Rust struct.2. State Management: Centralized, Action-Driven
#[live]
can be initialized or modified in DSL, suitable for configuration items and default values.#[rust]
are pure Rust state, not directly controlled by DSL, used to store runtime data, caches, etc.handle_event
, handle_actions
, handle_signal
), and never in draw_walk
.widget_ref.redraw(cx)
or area.redraw(cx)
to notify the framework that this part of UI needs to be redrawn.Arc<Mutex<T>>
or RwLock
to directly pass and modify data between different components to drive UI updates. This approach can introduce complexity, race conditions, and debugging difficulties.3. Event Handling and Communication: Action-Based Message Passing
AppMain::handle_event
, and passed through Widget tree via widget_ref.handle_event(cx, event, scope)
(usually up or down, depending on event_order
).cx.widget_action(widget_uid, path, YourActionEnum::Variant)
. widget_uid
identifies the sender, path
provides hierarchical information, YourActionEnum
is a custom enum containing specific information.MatchEvent::handle_actions
method. Helper methods like widget_ref.action_name(&actions)
or actions.find_widget_action(uid).cast()
are used to check and handle specific Actions sent by specific Widgets.Cx::post_action(YourSignalEnum::Variant)
to send a SignalToUI
type Action.MatchEvent::handle_signal
, then updates state and requests redraw.#[derive(Clone, DefaultNone, Debug)]
and include a None
variant to meet framework requirements.4. Rendering Paradigm: State-Driven, Declarative Drawing
draw_walk
method only reads current state and generates drawing instructions, should not have side effects or modify state.handle_event
/handle_actions
update state.redraw()
to mark the Area
that needs redrawing.Cx
collects all redraw requests at the appropriate time in the event loop.Event::Draw
event.draw_walk
is called, it reads the current state, and uses Cx2d
API to generate drawing instructions (usually filling uniforms and instances of Draw*
structs).DrawQuad
, DrawText
, DrawIcon
, DrawColor
and other structs encapsulate Shader and related uniforms. Their fn pixel
and fn vertex
define visual appearance.DrawList2d
: Internal optimization mechanism for batching draw calls.ViewOptimize::DrawList
/ Texture
: Allow caching static or infrequently changing UI parts to draw list or texture, avoiding recalculating layout and drawing every frame. CachedView
is a convenient wrapper.5. Widget Architecture: Composition and Derive Macros
#[derive(Live, LiveHook, Widget)]
to define Widgets.View
or other Widgets in live_design!
. Use #[deref]
to delegate core Widget
functionality (such as draw_walk
, handle_event
) to internal View
or other base Widgets.WidgetRef
and WidgetSet
(often type aliases like ButtonRef
, ButtonSet
) to find, access, and manipulate Widget instances.6. Asynchronous Operations
Cx::spawn_thread
to start background threads for time-consuming operations.mpsc
channel or custom ToUIReceiver
/Sender
pattern, after completion in background thread, send SignalToUI
Action back to UI thread through Cx::post_action
.handle_signal
, update state and request redraw.7. Dynamic UI Updates (Rust -> DSL)
widget_ref.apply_over(cx, live!{...})
. This allows applying local DSL fragments to existing Widget instances.8. Summary: Advantages of Makepad Paradigm
Following these paradigms helps write efficient, maintainable applications that conform to Makepad's design philosophy.
Identifying and avoiding unnecessary redraws is key to optimizing Makepad application performance. Here are some methods and perspectives to judge and handle unnecessary redraws:
1. Understanding Makepad's Redraw Mechanism
widget_ref.redraw(cx)
or area.redraw(cx)
, you're actually marking that Area
as "dirty".Cx
collects all areas marked as dirty.Cx
determines which Pass
es (render passes, usually corresponding to a window or a cached View) need redrawing because they contain dirty areas.DrawCall
s (drawing instructions) that intersect with dirty areas will be resent to the GPU.2. Signs of Unnecessary Redraws
log!()
statements in key places of handle_event
or draw_walk
, observe their calling frequency and timing. Pay special attention to whether draw_walk
is frequently called without state changes.3. Common Causes and Methods for Identifying Unnecessary Redraws
Modifying State in draw_walk
:
draw_walk
should only read state and draw. If state is modified in draw_walk
(even minor state), and that modification triggers redraw()
, it will cause an infinite redraw loop.draw_walk
implementations, ensure they don't modify any #[live]
or #[rust]
state fields, and don't call any methods that might indirectly trigger state changes or redraw.handle_event
or handle_actions
.Calling redraw()
Too Frequently:
Problem: Calling redraw()
in event handling even when state hasn't actually changed. Or unconditionally calling redraw()
in every NextFrame
event.
Identification: Before calling redraw()
, check if state has actually changed enough to require redrawing.
Solution:
Redraw Scope Too Large:
cx.debug.area(area, color)
in draw_walk
to visualize which Area
s are marked as dirty.redraw()
. For example, only call widget_ref.redraw(cx)
of child Widget that needs updating, or directly use child Widget's area.redraw(cx)
.Unnecessary Animation Triggers:
Animator
state transitions are unnecessarily triggered, even when there's no visual change, but redraw: true
in the apply
block still causes redraw. Or animator_handle_event
returns must_redraw()
but there's no visual change.animator_play
, animator_toggle
) are too frequent or called unnecessarily.redraw: true
from the apply
block (though this is rare).Live Design Updates Triggering Excessive Redraws:
after_apply
logic or child component's drawing might need optimization.Calling redraw()
on Area::Empty
or Invalid Area
:
redraw
on invalid areas is a meaningless operation.Area
is valid before calling redraw
(for example, whether Widget has been drawn at least once).if area.is_valid(cx) { area.redraw(cx); }
or if !widget_ref.is_empty() { widget_ref.redraw(cx); }
.Overusing redraw_all()
:
cx.redraw_all()
will redraw all content of all windows, should only be used for global state changes (like theme switching) or debugging.cx.redraw_all()
calls in the codebase.redraw()
calls.Debug Techniques
Comment Out redraw()
Calls: Temporarily comment out suspicious redraw(cx)
calls, observe if UI still updates as expected (possibly through other redraw
s). If UI stops updating, that redraw
is necessary; if UI still updates or some unrelated part stops updating, there may be a problem.
Color Debugging: Add a random or changing color to Widget's background at the beginning of draw_walk
, if this color changes when it shouldn't, it's being unnecessarily redrawn.
Log Recording: Add log!()
in key functions like handle_event
, handle_actions
, draw_walk
, record calling timing and related state, analyze calling frequency.
By combining understanding of Makepad's redraw mechanism with the judgment methods and debug techniques above, you can effectively locate and eliminate unnecessary redraws, thus optimizing application performance.
Robrix's core functionality depends on continuous communication with Matrix Homeserver, which is a typical I/O intensive operation. To maintain UI smoothness and responsiveness, Robrix adopts a clear synchronous (UI main thread) and asynchronous (background Tokio thread) separation architecture. Interaction between these two worlds is through a carefully designed message passing system.
1. Async Request and Response Model
Robrix's async interaction follows a standard "request-response/notification" model:
MatrixRequest
enum) and sends it to the background async task. UI thread doesn't wait for operation completion, but immediately returns to process other events.Action
(or other form of message) and sends it back to UI main thread.2. Request Processing Flow Detailed
From user triggering an action on UI, to backend completing processing, to UI updating, the flow is as follows:
handle_event
.MatrixRequest
: UI component creates a specific MatrixRequest
enum instance based on event type and current state. For example:
MatrixRequest::SendMessage { ... }
MatrixRequest::PaginateRoomTimeline { direction: Backwards, ... }
MatrixRequest::GetUserProfile { ... }
submit_async_request
): UI code calls submit_async_request(request)
.REQUEST_SENDER
): submit_async_request
internally gets global tokio::sync::mpsc::UnboundedSender<MatrixRequest>
(REQUEST_SENDER
), and sends request
to channel. This is a non-blocking operation (because channel is unbounded).async_worker
Receives: Background Tokio runtime's async_worker
task holds an UnboundedReceiver<MatrixRequest>
. It waits for new requests in a loop with recv().await
.tokio::spawn
): Upon receiving a request, async_worker
typically uses tokio::spawn
to start a new, specific async task to handle that request based on MatrixRequest
type (e.g., a dedicated task for sending messages, a dedicated task for pagination, etc.). This allows async_worker
itself to quickly return and receive the next request, achieving concurrent processing.matrix-sdk
's async methods to interact with Homeserver.Why Choose Specific Async Patterns?
tokio::sync::mpsc::unbounded_channel
(for MatrixRequest
):
crossbeam_channel::unbounded
(for TimelineUpdate
):
Receiver
can use try_recv
in non-async
contexts (like Makepad's handle_event
) for non-blocking reception, very suitable for UI thread.crossbeam
is generally considered higher performance than tokio::mpsc
in some scenarios (though difference may not be significant in this application).Sender
directly sends updates to corresponding RoomScreen
's Receiver
.tokio::sync::watch::channel
(for TimelineRequestSender
):
changed().await
only awakens waiters after value has actually changed.crossbeam_queue::SegQueue
(for PENDING_*_UPDATES
):
push
in background thread, pop
in UI thread.Event::Signal
, avoiding too frequent UI updates.3. Response Handling Strategy Detailed
After background task completion, results or state updates need to be notified back to UI thread.
Cx::post_action
+ SignalToUI
client.login(...)
returns Ok
or Err
).Action
enum value based on result (e.g. LoginAction::LoginSuccess
or LoginAction::LoginFailure(error_string)
).Cx::post_action(action)
to put Action into Makepad event queue.SignalToUI::set_ui_signal()
to wake up UI thread.App::handle_actions
(or other Widget's handle_actions
) matches the Action.AppState
or related Widget's #[rust]
state.self.ui.redraw(cx)
or specific Widget's redraw()
to trigger interface redraw.SegQueue
+ SignalToUI
)
RoomsListUpdate::AddRoom(...)
).push
update to corresponding global SegQueue
(e.g. PENDING_ROOM_UPDATES
).SignalToUI::set_ui_signal()
.Event::Signal
in handle_event
.pop
queue, get all pending updates.RoomsList
's all_rooms
and displayed_rooms
).redraw()
.crossbeam_channel
)
timeline_subscriber_handler
receives SDK's VectorDiff
.TimelineUpdate
(e.g. TimelineUpdate::NewItems{...}
).TimelineUpdate
through timeline_update_sender
.SignalToUI::set_ui_signal()
.RoomScreen
calls process_timeline_updates
in handle_event(Event::Signal)
.process_timeline_updates
try_recv
s updates from its update_receiver
.TimelineUiState
(such as items
vector) and call redraw()
.How to Synchronize UI State?
Core is Action/update message-driven state change:
AppState
fields, or RoomScreen
's TimelineUiState
, or data in cache).redraw()
to notify Makepad that Widget or entire UI needs redrawing to reflect new state.Recommended Patterns for Handling Network Failures and Delays:
LoginStatusModal
and show "Logging in..." in LoginScreen
, show LoadingPane
or TopSpace
in RoomScreen
).Err
returned by matrix-sdk
or other libraries into user-understandable error message strings when caught.LoginAction::LoginFailure(String)
).LoginStatusModal
text, or use enqueue_popup_notification
to show temporary notification).RequestConfig
when building Client
. SDK will return error after timeout.matrix-sdk
's local cache. Can still display cached data even when network is disconnected.SyncService
state).4. Common Anti-patterns and Avoidance Methods
Future
in handle_event
or draw_walk
and using .await
or synchronous blocking methods (like block_on
, recv().unwrap()
) to wait for result.TimelineUiState
), and modify state through single entry point (Action handling).In complex applications like Robrix that continuously interact with external services (Matrix Homeserver), clear and efficient state management is crucial. Robrix adopts layered state management and clear data flow patterns.
1. State Layering
Robrix's state management can be roughly divided into three layers:
a. Global Application State (AppState
in app.rs
)
logged_in: bool
: Whether user is logged in.window_geom: Option<WindowGeom>
: Current window size and position information.rooms_panel: RoomsPanelState
: Desktop version Dock layout state, including currently selected room (selected_room
), Dock layout itself (dock_state
), all open Tabs and their order (open_rooms
, room_order
).Scope::data.get::<AppState>()
or Scope::data.get_mut::<AppState>()()
for read/write access.App
component's handle_actions
or its direct child components (such as MainDesktopUI
through RoomsPanelAction
).b. Room-Level State (TimelineUiState
in room_screen.rs
, stored in global TIMELINE_STATES
Mutex)
room_id
: Room identifier.user_power
: Current user's permission level in the room.fully_paginated
: Whether timeline has been loaded to the very beginning.items: Vector<Arc<TimelineItem>>
: List of currently loaded timeline events.content_drawn_since_last_update
, profile_drawn_since_last_update
: Cache markers for optimizing PortalList
drawing.media_cache
: Cache for media files in this room.replying_to
, editing_event
: Currently replying to or editing message state.saved_state
: Saves scroll position, input box content, etc., to restore when switching back to room.latest_own_user_receipt
: Latest read receipt from self.TIMELINE_STATES: Mutex<BTreeMap<OwnedRoomId, TimelineUiState>>
.RoomScreen
displays a room (show_timeline
), it remove
s that room's TimelineUiState
from TIMELINE_STATES
and stores it in RoomScreen
's self.tl_state
field (type is Option<TimelineUiState>
).RoomScreen
hides a room (hide_timeline
, usually called in set_displayed_room
or Drop
), it insert
s the TimelineUiState
from self.tl_state
(including updated saved_state
) back into global TIMELINE_STATES
.RoomScreen
instance holding self.tl_state
when processing TimelineUpdate
or user interaction.timeline_subscriber_handler
sends TimelineUpdate
to corresponding RoomScreen
through crossbeam_channel
.RoomScreen
processes TimelineUpdate
and modifies self.tl_state
.c. Component-Level State (#[rust]
fields in Widgets)
saved_state
mechanism).AvatarRow::buttons
: Cached avatar Widget references and drawing state.EditingPane::info
: Current editing message information.LoadingPane::state
: Loading panel's current task state.MentionableTextInput::current_mention_start_index
, is_searching
, possible_mentions
: Temporary states related to @mention
.NewMessageContextMenu::details
: Message details associated with current context menu.UserProfileSlidingPane::info
: Profile information of currently displayed user.visible: bool
, is_animating_out: bool
controlling visibility and animation.handle_event
, handle_actions
, draw_walk
, or custom methods.WidgetRef
(such as show()
, hide()
, set_text()
).cx.widget_action
.Scope::props
.Best Practices for State Change Propagation:
AppState
/TimelineUiState
update -> redraw()
-> draw_walk
reads state and renders.Scope::props
to pass read-only data.Mutex
or other synchronization primitives, but these should be used minimally in UI thread).saved_state
in TimelineUiState
).2. Data Flow Diagram
This is a simplified data flow diagram showing key paths from Matrix server to UI components:
![[Screenshot 2025-04-21 at 17.09.47.png]]
Key Points and Bottleneck Analysis:
avatar_cache
, media_cache
, user_profile_cache
, room_member_manager
, TimelineUiState
(holding media_cache
).VectorDiff
into TimelineUpdate
.RoomScreen
processing TimelineUpdate
to update items
vector. RoomsList
processing RoomsListUpdate
to update all_rooms
and displayed_rooms
. Formatting and rendering based on state in draw_walk
.Event::Signal
help mitigate).@mention
suggestion list might affect UI responsiveness (though optimization through passing references via props has improved).draw_walk
Complexity: If draw_walk
contains too many conditional checks or complex drawing logic, rendering might slow down.Mutex
use in UI thread, if global state (like TIMELINE_STATES
) lock contention is fierce (unlikely in current design), it might become a bottleneck.Event::Signal
and queue mechanisms to process multiple backend messages in one UI update.PortalList
): Already in use, ensure its configuration is reasonable, only rendering visible items.draw_walk
Simplification: Keep draw_walk
as simple as possible, move calculations to state update phase.3. State Management Rules
Animator
) to implement smooth transition effects (such as EditingPane
's slide in/out).Arc
to share immutable data (such as TimelineItem
, Vec<RoomMember>
).&T
or &Arc<T>
) through Scope::props
rather than cloning data.draw_walk
, preferably preprocess during state update.Robrix's UI consists of a series of reusable Makepad Widget components. By analyzing the design of these components, we can extract design patterns commonly used in Robrix (and other Makepad-based applications).
1. Basic UI Component Patterns
These patterns focus on building the basic elements that make up the interface.
a. Reusable Form Components (RobrixTextInput
, RobrixIconButton
)
TextInput
, Button
), define a set of visual styles (colors, borders, fonts, padding, etc.) through live_design!
that conform to Robrix's visual style. These customized components (RobrixTextInput
, RobrixIconButton
) are widely reused throughout the application to ensure UI consistency.live_design!
block in shared/styles.rs
or component's own file.<BaseWidget>
syntax to inherit from base component.draw_bg
, draw_text
, draw_icon
and other draw
blocks to apply custom styles.walk
, layout
, padding
, margin
and other attributes.RobrixTextInput
unifies input box appearance; RobrixIconButton
provides buttons with icons and specific background/border styles.b. Image and Media Components (Avatar
, TextOrImage
, HtmlOrPlaintext
)
flow: Overlay
to make internal views overlap.#[rust]
state (like Avatar::info
, TextOrImage::status
).draw_walk
, set internal views' visible
property based on state.Avatar::show_text
, Avatar::show_image
, HtmlOrPlaintext::show_html
) to update state and trigger redraw.avatar_cache
, media_cache
), update component state through Action after data loads.Avatar
displays first letter while avatar loads, then displays image after loading. TextOrImage
displays text when image fails to load or hasn't loaded.c. Special Function Extension Components (MentionableTextInput
)
MentionableTextInput
is a typical example, based on CommandTextInput
, adding specific behaviors like @mention
triggering, popup suggestion list, interaction with RoomMemberManager
, etc.<BaseWidget>
in live_design!
to inherit from base component.#[deref]
in Rust struct.#[rust]
state to manage new functionality (like current_mention_start_index
, is_searching
).handle_event
and handle_actions
, implementing new behaviors.MentionableTextInputAction
) to communicate with other components.Scope::props
.MentionableTextInput
adds complete @mention
logic on top of CommandTextInput
.2. Interactive Component Design Patterns
These patterns focus on handling more complex UI interaction flows.
a. Modal Dialogs and Floating Layers (LoginStatusModal
, VerificationModal
, LoadingPane
, UserProfileSlidingPane
)
App
or RoomScreen
, using flow: Overlay
layout to overlay on top of other content. Their visibility is controlled by parent component or global state. Often accompanied by animation effects (fade in/out, slide in/out) to enhance user experience.live_design!
.#[animator]
to define show/hide animations (show
/hide
states).LoginAction::Status
, VerificationAction::RequestReceived
).LoginStatusModalAction::Close
, VerificationModalAction::Close
, EditingPaneAction::Hide
).hide()
or close()
method when handling close Action (usually triggering hide animation).bg_view
), or Makepad's Modal
component itself handles event capture.LoginStatusModal
shows status during login; VerificationModal
handles device verification flow; EditingPane
provides message editing interface.b. Context Menus and Tooltips (NewMessageContextMenu
, CalloutTooltip
)
App
).Message
, AvatarRow
, ReactionList
) detects trigger condition (like Hit::FingerLongPress
, Hit::FingerHoverIn
) in handle_event
.MessageAction::OpenMessageContextMenu
, TooltipAction::HoverIn
), including trigger position (abs_pos
) or target element rectangle (widget_rect
).App
or parent component handles the Action, calls context menu/tooltip's show()
or show_with_options()
method.show()
method internally calculates final display position based on passed position/rectangle information, combined with expected size (usually needs to consider avoiding window boundaries), and positions through apply_over
setting margin
.TooltipAction::HoverOut
or other mechanism.NewMessageContextMenu
provides message operations; CalloutTooltip
displays read users or reaction user lists.c. Lists and Virtual Scrolling (Timeline
using PortalList
, RoomsList
using PortalList
)
PortalList
Widget.draw_walk
:
list.set_item_range()
to tell PortalList
total number of items.while let Some(item_id) = list.next_visible_item(cx)
loop:
item_id
.list.item(cx, item_id, template_live_id)
to get or create (reuse) a list item Widget instance.item.draw_all(cx, &mut scope)
to draw the item.PortalList
internally handles scroll events and calculates which item_id
s need to be drawn in next frame.RoomScreen
's Timeline
uses PortalList
to display messages; RoomsList
uses PortalList
to display room previews.3. Component Communication Patterns
a. Parent-Child Component Communication:
WidgetRef
, directly calls its public methods (like set_text
, show
, hide
, apply_over
). Suitable for direct imperative interactions.Scope::props
: Parent component passes read-only context data through Scope::with_props
when calling child component's handle_event
/draw_walk
. Suitable for passing context information needed for rendering or event handling.cx.widget_action
): Child component uses cx.widget_action
to send an Action in its handle_event
/handle_actions
. Parent component matches and handles the Action in its handle_actions
. This is the most common and recommended way.b. Sibling Component Communication:
AppState
): If information is truly global, one component modifies AppState
, another component reads AppState
in draw_walk
or handle_event
to respond to changes (needs to work with redraw()
). Suitable for global settings, etc.WidgetRef
. This usually leads to tight coupling and should be avoided.c. Component and Global State Communication:
handle_event
or draw_walk
through scope.data.get::<AppState>()
.App
, App::handle_actions
is responsible for modifying AppState
. Or, for some specific global states (like caches), can indirectly trigger updates by calling global singleton (like RoomMemberManager::instance()
) methods (backend will notify UI through Action after update).Building a smooth, responsive Matrix client like Robrix requires continuous attention to performance. While the Makepad framework itself provides efficient rendering mechanisms, application-level logic and data processing can still introduce performance bottlenecks. This guide aims to provide methods for evaluating and optimizing Robrix's performance.
1. Performance Metrics and Evaluation
To effectively tune performance, we need to define key metrics and establish evaluation methods.
a. Key Performance Indicators (KPIs):
RoomScreen
. This affects scrolling smoothness.RoomsList
) and timeline (Timeline
), target is close to 60 FPS.@mention
suggestion list popup and filtering response time.b. Performance Benchmarking and Evaluation Methods:
std::time::Instant
) in critical code paths (like draw_walk
, handle_event
, Action handling, background tasks), record time-consuming operations. Makepad's log!
macro automatically includes timestamps.perf
, macOS's Instruments, Windows's Performance Analyzer) or specialized Rust analysis tools (such as flamegraph
, cargo-profiler
).2. Common Performance Issues and Solutions
a. Excessive Redrawing (Over-drawing):
draw_walk
, triggering logic that seems to need redrawing even when state hasn't changed.redraw()
or cx.redraw_area()
.Animator
) improperly configured, causing continuous redraw.draw_walk
: Ensure draw_walk
only depends on current state for drawing, move state update logic to handle_event
/handle_actions
.redraw()
after relevant state has actually changed.cx.redraw_area(area)
instead of global self.redraw(cx)
, only redrawing Widget areas that truly need updating.CachedView
or manual caching (like drawn
flag in AvatarRow
).b. Large Message Loading and Rendering Optimization:
PortalList
tries to render too many items at once (if misconfigured or data volume is extreme).draw_walk
is too complex or time-consuming (e.g., complex HTML parsing/rendering, synchronous image decoding).draw_walk
for each visible message item.PortalList
Optimization: Ensure PortalList
is correctly configured and working effectively. Adjust its caching strategy (if configurable).TimelineUiState
(i.e. in process_timeline_updates
), avoiding repeated calculation in draw_walk
.draw_walk
, triggering redraw through Action after loading completes. TextOrImage
component embodies this pattern.Html
Widget has internal optimizations, but still need to avoid passing extremely complex or malformed HTML.ItemDrawnStatus
(as shown in RoomScreen
) to mark already drawn content and Profile, avoiding unnecessarily refilling Widget data in subsequent draw_walk
.c. Memory Usage Optimization:
Arc
or other reference counting pointers can't decrease to zero due to circular references or long-lived objects (like undestroyed Widget instances, entries in global cache).Vec<RoomMember>
, Vec<TimelineItem>
).RoomScreen
, ensure lifecycle of room-related state (like member list Arc
) is bound to RoomScreen
instance (or its representation in global state), and correctly release references and unsubscribe when room is closed/switched. Carefully check Drop
implementation.AVATAR_NEW_CACHE
, MediaCache
), limiting cache size.&
, &mut
, &Arc
). Use Cow
(Clone-on-Write) to avoid unnecessary string cloning.Arc
to share large read-only data (like TimelineItem
, Vec<RoomMember>
).3. Mobile Device Performance Considerations
Mobile devices are typically resource-constrained (CPU, memory, battery), requiring special attention to performance optimization.
a. Low-Power Device Optimization:
AdaptiveView
can be used for this purpose.b. Battery Usage Optimization:
SignalToUI
when UI actually needs updating.c. Mobile Network Environment Optimization:
4. Recommended Capture Tool: charles proxy
Matrix is a complex distributed system, and clients will inevitably encounter various errors when interacting with Homeservers and processing local data. Robust error handling and recovery strategies are essential for providing a stable, reliable user experience.
1. Error Classification and Handling Strategies
Errors that might be encountered in Robrix can be roughly categorized as follows, each requiring different handling strategies:
a. Network Errors
matrix-sdk
network layer, reqwest
, system network stack.PopupNotification
or in specific view (like login page, room list top).LoginScreen
. If background refresh token fails, may need to mark session as invalid and force user to log in again.SyncService
should catch network errors, log them, and decide whether to retry or enter error state based on error type.matrix-sdk
's network-related methods (like login
, sync
, send
, get_media_content
), use match
or ?
operator to handle Result
, and differentiate handling based on specific types of matrix_sdk::Error
(like Http
, ClientApi
).b. Data Errors
matrix-sdk
(deserialization, encryption), local storage (sled
or sqlite
), user input parsing.TextInput
's handle_actions
), such as input box border turning red, displaying hint message.TimelineItemContent::UnableToDecrypt
), should clearly display "Unable to decrypt message" placeholder in timeline, rather than crashing or hiding that message.Result
or Option
to handle potential failures.matrix-sdk
's handling mechanisms, and display corresponding state in UI.Result::map_err
, Option::ok_or
etc. to convert error types, and appropriate match
handling.c. UI Interaction Errors
handle_event
/ handle_actions
logic.RoomScreen
). Disable related buttons or input boxes.PopupNotification
.UserPowerLevels
from backend).TimelineItem
) exist and are in correct state.assert!
, debug_assert!
) to catch logical errors during development.handle_actions
, get required state from AppState
or TimelineUiState
for checking. Use if let Some(...)
or guard
statements for early return.2. Graceful Degradation Mechanism
When encountering errors or resource limitations, application should maintain partial functionality where possible, rather than completely crashing or becoming unusable.
a. Local Functionality During Network Outages:
matrix-sdk
's local cache (SQLite storage).SyncService
is in Error
or Offline
state.SyncService
state and displaying indicators on interface).b. Partial Display Strategy When Data Incomplete:
RoomScreen
displays TimelineItemContent::UnableToDecrypt
placeholder.Avatar
component displays user's first letter when image fails to load or is unavailable.RoomPreview
or UserProfileSlidingPane
displays user ID when username is missing.c. Fallback Options When Services Unavailable:
location.rs
notifies UI through LocationAction::Error
when initialization or location acquisition fails.RoomScreen
disables send location button and displays error message in LocationPreview
when handling LocationAction::Error
.3. Error Feedback and User Experience
How error information is communicated to users directly affects user experience.
a. Error Message Design Principles:
PopupNotification
), avoid overusing modal dialogs.b. Error Retry and Recovery Flow:
c. Development Environment vs Production Environment Error Handling Differences:
log!
, error!
, warn!
).debug_assert!
at critical logic points to catch errors during development.panic!
in some situations to quickly expose problems (but should remove or replace with error handling before committing code).panic!
, all foreseeable errors should be caught and gracefully handled.High-quality software requires comprehensive testing. For applications like Robrix that involve complex UI interactions, asynchronous network communications, and state management, establishing a clear testing strategy is particularly important.
1. Testing Types and Scope
To cover different code levels and functional interactions, Robrix should adopt a layered testing strategy:
a. Unit Tests
#[cfg(test)] mod tests { ... }
).#[test]
macro, possibly using mockall
or hand-written mock objects to simulate dependencies.utils.rs
(such as human_readable_list
, linkify
, ends_with_href
).event_preview.rs
.persistent_state.rs
(may need to mock the file system).sliding_sync.rs
(such as username_to_full_user_id
).RoomDisplayFilterBuilder
.b. Integration Tests
tests/
directory at the project root.#[test]
macro, may need to set up mocked backend environment (such as mock Matrix Client or specific Action handlers), or start partial backend services in tests (like testing RoomMemberManager
).RoomScreen
correctly updates its TimelineUiState
after receiving TimelineUpdate::NewItems
.RoomPreview
in RoomsList
correctly sends RoomsListAction::Selected
, and whether MainDesktopUI
or App
correctly handles that Action to switch rooms.MentionableTextInput
correctly updates its suggestion list after receiving RoomMembersUpdated
Action.App
correctly switches views after LoginScreen
initiates login request (MatrixRequest::Login
) and backend returns simulated LoginAction::LoginSuccess
or LoginFailure
.RoomMemberManager
's subscription and unsubscription logic correctly updates internal state and notifies subscribers (as in its own tests).c. End-to-End Tests (E2E Tests)
thirtyfour
(WebDriver), autogui
, SikuliX
, etc. (may need platform-specific adaptation).HomeScreen
displays.RoomsList
, clicking a room, verifying whether RoomScreen
loads and displays correct content.RoomInputBar
, clicking send, verifying whether message appears in Timeline
(needs to mock backend response).UserProfileSlidingPane
pops up and displays correct information.2. Makepad Component Testing Techniques
Directly testing Makepad Widget rendering and interaction logic is relatively difficult, as they deeply depend on Cx
context and event loop. Here are some feasible strategies:
a. Component Rendering Tests (Visual Regression Testing - Difficult):
b. Event Triggering and Response Testing (Logic Level):
Cx
or provide a basic Cx
environment, like create_test_cx
in room_member_manager.rs
tests).Event
enum instances (such as Event::Actions
, Event::FingerDown
, Event::KeyDown
).handle_event
: Pass simulated Event
to Widget's handle_event
method.cx.capture_actions(|cx| ...)
to capture WidgetAction
s emitted by Widget when handling events.#[rust]
state changed as expected.Cx
and Scope
can be complex. handle_event
may depend on Area
information produced by draw_walk
, which is difficult to precisely simulate in test environment.c. Asynchronous Operation Testing (Combined with Integration Tests):
submit_async_request
.async_worker
, but directly capture or mock MatrixRequest
. Then, manually construct expected response Action
(like LoginAction::LoginSuccess
).cx.push_action(response_action)
to push mocked response Action into event queue.handle_actions
: Manually call tested Widget's (usually App
or RoomScreen
) handle_actions
method.redraw()
was called.3. Test Environment and Data
a. Mocking Matrix Server Responses:
matrix-sdk-test
crate: Testing tool library provided by Matrix Rust SDK, containing mock server (MockServer
) and response builders, can simulate success and failure responses for various API endpoints.wiremock
or other HTTP mocking libraries, intercepting and responding to specific Matrix API requests.b. Test Data Generation Strategy:
proptest
to generate large amounts of different types of test data (e.g., messages of different lengths, different numbers of members, usernames containing special characters).faker-rs
to generate realistic mock data (usernames, message contents, etc.).c. Performance Benchmark Testing Environment:
criterion
crate: Rust's standard benchmark testing library, for measuring execution time of small code segments.