Zola2025-01-09T00:00:00+00:00https://robius.rs/atom.xmlOur Roadmaps for Project Robius & Robrix in 20252025-01-09T00:00:00+00:002025-01-09T00:00:00+00:00
Unknown
https://robius.rs/blog/robius-roadmap-2025/<p><em>Author: <a href="https://github.com/kevinaboos">Kevin Boos</a>. Last updated January 16th, 2024.</em> <a href="https://www.reddit.com/r/rust/comments/1i2wq5j/project_robius_in_2024_another_year_of_progress/">Discuss this on Reddit</a>.</p>
<p>‼️ <strong>This is a continuation of <a href="../robius-retrospective-2024">our previous post</a></strong>.</p>
<p>We've just completed the first full year of work on <strong>Project Robius</strong>: an open-source decentralized endeavor to enable developers to write immersive, fully-featured apps in pure 🦀 Rust 🦀 that work seamlessly across all major platforms.</p>
<p>In this companion post, we'll take a look at our roadmap for 2025 and beyond, both for Project Robius as a whole and for the <a href="https://github.com/project-robius/robrix">Robrix</a> app specifically.</p>
<h2 id="project-robius-roadmap-for-2025">Project Robius Roadmap for 2025</h2>
<p>Project Robius in 2025 aims to continue the work we've begun in 2024 to improve the overall app dev experience in Rust.</p>
<h4 id="more-rust-abstractions-for-platform-features">More Rust abstractions for platform features</h4>
<p>As a technical organization, our primary ongoing focus will be to keep creating and publishing as many high-quality platform feature abstraction crates as possible.
Now that we have a foundation established with <a href="../robius-retrospective-2024#1-robius-crates-for-platform-feature-abstractions">several existing crates</a>, we anticipate being able to make faster progress, especially with the expected addition of more contributors.
Our targeted platform features include (in rough priority order):</p>
<ul>
<li>File/image/media picker (in progress)</li>
<li>Native system notifications (in progress)</li>
<li>Toasts, pop-up messages, status bar icons
<ul>
<li>We have implemented this in Makepad, but not with native widgets commonly used on Mobile platforms</li>
</ul>
</li>
<li>Spawning long-running background tasks/services</li>
<li>System file/media store</li>
<li>Native context menus
<ul>
<li>Same status as toasts above.</li>
</ul>
</li>
<li>Camera access & configuration</li>
<li>Audio input (microphone)</li>
<li>System theming choices (e.g., dark mode, key colors)</li>
<li>Connectivity manager/subscriber</li>
<li>Power/battery status</li>
<li>Haptics/vibration</li>
</ul>
<h4 id="better-more-automated-build-tooling">Better, more automated build tooling</h4>
<p>Another topic dear to our hearts is build tooling.
We aim to improve the state of build tools such that the app developer themself can be relieved from the burden of managing and figuring out platform-specific details, such as which permissions/entitlements their app requires to build and run properly on mobile platforms.
Ideally, we'd like to be able to auto-generate a fully-formed Android XML manifest or Apple <code>Info.plist</code> file with all of the necessary permissions that an app requires, without requiring the app dev to possess expert knowledge about the requirements of their app's dependencies and transitive dependencies.</p>
<p>One such idea for realizing this is to make each <code>robius-*</code> platform feature abstraction crate automatically emit its required permissions during the build process.
Exactly <em>how</em> to export and encode this information is still up in the air, but we have discussed leveraging a linker-based approach similar to what <a href="https://github.com/DioxusLabs/manganis">Dioxus's manganis project</a> does to encode resource/asset paths into special linker sections.
This would allow a top-level tool to run after the <code>cargo build</code> process, and inspect the binary's special linker sections in order to automatically generate a full permissions/entitlements file for the given target platform.
We envision that this could also be used for other arbitrary UI toolkits, not just Makepad, as well as emitted by other platform abstraction crates outside of the <code>robius-*</code> organization.</p>
<h4 id="effortless-integration-with-other-ui-toolkits">Effortless integration with other UI toolkits</h4>
<p>In addition, we wish to explore deeper integration and first-class compatibility (and testing pipelines) with other Rust UI toolkits, e.g., Dioxus, eGUI, and more.
Our first year of development has been centered on Makepad, in the sense that we've built two full-size Makepad apps, contributed significantly to Makepad itself, and have focused on test-driving our crates using Makepad apps (see <a href="https://github.com/project-robius/robius-demo-simple"><code>robius-demo-simple</code></a>).
Thus, using Robius components in a Makepad app is quite easy for the app developer.
All they need to do is add a dependency on a special "marker" crate that auto-configures all <code>robius-*</code> crates to work with Makepad, as shown below in Robrix's <code>Cargo.toml</code>:</p>
<pre data-lang="toml" style="background-color:#eff1f5;color:#4f5b66;" class="language-toml "><code class="language-toml" data-lang="toml"><span>[dependencies]
</span><span style="color:#bf616a;">makepad-widgets </span><span>= { </span><span style="color:#bf616a;">git </span><span>= "</span><span style="color:#a3be8c;">https://github.com/makepad/makepad</span><span>", </span><span style="color:#bf616a;">branch </span><span>= "</span><span style="color:#a3be8c;">rik</span><span>" }
</span><span style="color:#bf616a;">robius-open </span><span>= "</span><span style="color:#a3be8c;">0.1.2</span><span>"
</span><span style="color:#bf616a;">robius-directories </span><span>= "</span><span style="color:#a3be8c;">5.0.1</span><span>"
</span><span style="color:#bf616a;">robius-location </span><span>= { </span><span style="color:#bf616a;">git </span><span>= "</span><span style="color:#a3be8c;">https://github.com/project-robius/robius-location</span><span>" }
</span><span>
</span><span style="color:#a7adba;">## Including this crate automatically configures all `robius-*` crates to work with Makepad.
</span><span style="color:#bf616a;">robius-use-makepad </span><span>= "</span><span style="color:#a3be8c;">0.1.1</span><span>"
</span></code></pre>
<p>Now that we have successfully realized several platform feature abstraction crates, we would like to ensure that these can be easily utilized by apps built in other UI toolkits.
For example, one specific secondary goal for this year is to explore how <code>robius-*</code> crates could comprise Dioxus's <a href="https://dioxuslabs.com/learn/0.5/contributing/roadmap/#mobile"><code>dioxus-std</code></a> library and fill in the gaps in their mobile platform support.</p>
<p>Another related goal is to design a UI-focused concurrency management library with an interface that helps app devs easily write high-performance apps that never block or bog down the main UI thread with long-running operations.
We envision easy interfaces to offload code to background threads or async tasks, as well as for exchanging data between these background contexts and the performance-sensitive the UI main thread.
The inability to easily invoke async functions from the UI main thread (without causing performance hiccups) is a long-running frustration we have had when developing Robrix, as many SDKs are written with a hard dependency on an async executor, typically tokio.
This serves as strong motivation to ameliorate the overly-complex code patterns shown in the diagram below, in which Robrix's structure of multiple execution contexts with myriad distinct communication mechanisms between them must be <em>manually</em> managed.</p>
<a href="/blog/robrix_concurrency_diagram.png">
<img style="width:98%" src="/blog/robrix_concurrency_diagram.png" alt="Diagram of how Robrix must manually manage mixed concurrency contexts">
</a>
<blockquote>
<p>To understand this concurrency challenge in more detail, <a href="https://youtu.be/DO5C7aITVyU?si=N_10UZBCR5g-w2D4&t=1390">watch this presentation on Robrix (starting from 23:10)</a> or <a href="https://github.com/project-robius/files/blob/main/GOSIM%20China%202024/Robrix%20Talk%20GOSIM%20China%20October%2017%2C%202024.pdf">check out slides 26-33 here</a>.</p>
</blockquote>
<p>A key component of this library is an abstract <em>compile-time token</em> that statically ensures whether code is executing within the context of the main UI thread context.
Such a type must be both non-<code>Send</code> and non-<code>Sync</code>, and only possible to construct on the main UI thread.
This token is necessary because most platforms require many of their platform-provided APIs to be invoked on the main thread, and it's significantly better to check this at compile time than via a runtime assertion.
We have realized this for Makepad via a mutable reference to the <a href="https://github.com/makepad/makepad/blob/0084948c176a99740af92a71578543c3fcc0b63f/platform/src/cx.rs#L55">context type</a> <code>&mut Cx</code>, which is created only on the main UI thread and then passed as a mutable reference to all of the <a href="https://github.com/makepad/makepad/blob/0084948c176a99740af92a71578543c3fcc0b63f/widgets/src/widget.rs#L45-L110">event handlers and draw routines</a>.</p>
<p>Here's a simplified example of how we leverage this technique in Robrix to implement an efficient cache for user profile information, while avoiding the need to acquire any locks on the main UI thread.</p>
<pre data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#a7adba;">/// Returns the cached user profile for the given user ID ...
</span><span style="color:#a7adba;">///
</span><span style="color:#a7adba;">/// This function requires passing in a reference to `Cx`,
</span><span style="color:#a7adba;">/// which isn't used, but acts as a guarantee that this function
</span><span style="color:#a7adba;">/// must only be called by the main UI thread.
</span><span style="color:#b48ead;">pub fn </span><span style="color:#8fa1b3;">get_user_profile</span><span>(
</span><span> </span><span style="color:#bf616a;">_cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx,
</span><span> </span><span style="color:#bf616a;">user_id</span><span>: OwnedUserId,
</span><span> </span><span style="color:#bf616a;">fetch_if_missing</span><span>: </span><span style="color:#b48ead;">bool</span><span>,
</span><span>) -> Option<UserProfile> {
</span><span> </span><span style="color:#a7adba;">// access the TLS cache, defined below.
</span><span> ...
</span><span>}
</span><span>
</span><span>thread_local! {
</span><span> </span><span style="color:#b48ead;">static </span><span style="color:#d08770;">USER_PROFILE_CACHE</span><span>: RefCell<BTreeMap<OwnedUserId, UserProfileCacheEntry>> = </span><span style="color:#b48ead;">const </span><span>{ ... };
</span><span>}
</span></code></pre>
<p>Robrix also uses this to statically ensure that a location initialization function can only be invoked from the main UI thread:</p>
<pre data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#a7adba;">/// Starts listening for location requests and updates to the latest device location.
</span><span style="color:#a7adba;">/// ...
</span><span style="color:#b48ead;">pub fn </span><span style="color:#8fa1b3;">init_location_subscriber</span><span>(</span><span style="color:#bf616a;">_cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx) -> Result<(), robius_location::Error> {
</span><span> ...
</span><span>}
</span></code></pre>
<p>With a proper UI-agnostic abstraction for a statically-known thread context marker, we can do this not just in the Makepad app logic, but also within the <code>robius-*</code> platform feature crates themselves.
Finally, while this sort of concurrency library and thread context abstractions are highly desirable, it's also admittedly a longer-term goal that merits major effort beyond just 2025.</p>
<h4 id="organizing-more-conferences-meet-ups">Organizing more conferences & meet-ups</h4>
<p>On the organizational side, we intend to sponsor two more conferences for open-source Rust development and host informal Rust app dev unconferences co-located with those conferences.
These will be scheduled similarly to <a href="../robius-retrospective-2024#4-cross-collaboration-with-other-ui-and-app-dev-orgs">the ones we hosted in 2024</a>:
the first will be <a href="https://rustweek.org/">RustWeek 2025</a> (formerly "RustNL") in the Netherlands in May, and the second will be <a href="https://china2024.gosim.org/">GOSIM China</a> in autumn 2025.
With these (un)conferences, we aim to bring community members together again to collaborate, share ideas, and to further advance the state-of-the-art for App Dev and UI in Rust.</p>
<hr style="border: none; width: 100%; color: #000000; background-color: #000000; height: 1px;" >
<h2 id="robrix-roadmap-for-2025-and-beyond">Robrix Roadmap for 2025 and beyond</h2>
<p>As discussed in <a href="../robius-retrospective-2024#robrix-an-up-and-coming-matrix-chat-client-for-power-users">our previous post</a>, Robrix is an up-and-coming Matrix chat client for power users, and serves as a "flagship" Robius app to drive the development priorities of various Robius components.</p>
<p>While Robrix is off to a strong start, we still have a long way to go, and we have a lot more cool features in mind beyond just Matrix chat support.
We have planned several high-level phases of Robrix development over the next 18-24 months:</p>
<ol>
<li><font color="gray"><em>[Q1 2025]</em></font> Release an alpha version of Robrix with most fundamental Matrix features available.
<ul>
<li>Realize sufficient functionality to be usable as a daily driver, but not yet to be a complete replacement for existing clients.</li>
<li>This is nearly complete! See <a href="https://github.com/project-robius/robrix/milestone/1">Milestone 1</a> on our GitHub page.</li>
</ul>
</li>
<li><font color="gray"><em>[Summer 2025]</em></font> Publish Robrix v1.0 with full Matrix functionality, for "power" users.
<ul>
<li>Offer a responsive UI design with a dockable, multi-tab view of many rooms side-by-side, which also adapts to varying screen sizes (mobile, desktop, etc).
<ul>
<li>✅ This is already complete! (<a href="../robius-retrospective-2024#robrix-an-up-and-coming-matrix-chat-client-for-power-users">as described in our previous post</a>)</li>
</ul>
</li>
<li>Achieve feature parity with existing major clients, including administrative features like a full settings pane, session management, room creation/admin, message search, threads, spaces, etc.
<ul>
<li>See <a href="https://github.com/project-robius/robrix/milestone/2">Milestone 2</a> on our GitHub page for more details.</li>
<li>Generally, these features are <em>not</em> drivers of Robius development, as they don't require complex platform features, so they were of a lower priority initially.</li>
</ul>
</li>
<li>Distribute Robrix app bundles to platform app stores and package managers.</li>
</ul>
</li>
<li><font color="gray"><em>[Q3 2025]</em></font> Integrate local LLM runtimes (like <a href="../robius-retrospective-2024#moly-chat-with-local-llms-and-custom-ai-agents">Moly</a>) for powerful, advanced convenience features.
<ul>
<li>LLMs or AI agents can summarize conversations, analyze important topics, and extract key action items from "what you missed" after a holiday. Here's a UI prototype: <br>
<a href="/blog/robrix_moly_prototype.png">
<img style="width: 50%" alt="A prototype UI design for AI LLMs alongside Matrix rooms in Robrix" src="/blog/robrix_moly_prototype.png" />
</a></li>
<li>AI chatbots can assist newcomers in large open-source projects by auto-answering FAQs, either privately or publicly to allow for additional interaction from real expert users.</li>
<li>Key point: <em>fully-local</em> LLM runtimes <strong>cannot jeopardize end-to-end encrypted (E2EE) rooms or user data sovereignty</strong>, so you can utilize LLMs with confidence that your privacy is being honored.</li>
</ul>
</li>
<li><font color="gray"><em>[Late 2025]</em></font> Go beyond Matrix: Robrix as a central "hub" for federated & open-source services
<ul>
<li>Collect multiple services into a unified app view, including ActivityPub-based microblogs (e.g., <a href="https://joinmastodon.org/">Mastodon</a>), views of source code and related issues/pull requests, discussion forums (e.g., <a href="https://join-lemmy.org/">Lemmy</a>), and more.
<ul>
<li>The exact set of supported services are TBD.</li>
</ul>
</li>
<li>The availability of many services in a single app context can enable unique combo features, such as a combined activity feed of notifications + news from various sources, or easy one-click broadcasting of project updates to multiple communities across different services.</li>
</ul>
</li>
<li><font color="gray"><em>[Long-term]</em></font> Explore how to use decentralized identity providers like <a href="https://openwallet.foundation/">OpenWallet</a> to login to Robrix-supported services.
<ul>
<li>Use Robrix as the first experimental testing ground for integrating a device-local wallet app as an ID provider for Matrix authentication.</li>
<li>For more info, check out <a href="https://www.youtube.com/watch?v=eq9pnYB5-Xk">this presentation by Wenjing Chu, an OpenWallet expert</a> from the Matrix Conference 2024.</li>
</ul>
</li>
</ol>
<p>While many of these are larger endeavors, we anticipate being able to complete at least milestones 1, 2, and 3 by the end of this coming year.</p>
<h2 id="acknowledgments">Acknowledgments</h2>
<p>I'd like to thank the following key people who have been instrumental to the success of Project Robius over the past year, and who will undoubtedly help it flourish in 2025.</p>
<ul>
<li>The Makepad team: <a href="https://x.com/rikarends">Rik Arends</a>, <a href="https://github.com/ejpbruel2">Eddy Bruël</a>, <a href="https://twitter.com/SebMichailidis">Sebastian Michailidis</a></li>
<li><a href="https://github.com/tsoutsman">Klim Tsoutsman</a></li>
<li><a href="https://www.wyeworks.com/">WyeWorks</a> developers: <a href="https://github.com/jmbejar">Jorge Bejar</a>, <a href="https://github.com/joulei">Julián Montes de Oca</a>, <a href="https://github.com/fmzbl">Facundo Mendizábal</a></li>
<li><a href="https://github.com/ZhangHanDong">Alex Zhang (ZhangHanDong)</a> and his team members: <a href="https://github.com/alanpoon">@alanpoon</a>, <a href="https://github.com/aaravlu">@aaravlu</a>, <a href="https://github.com/tyreseluo">@tyreseluo</a>, <a href="https://github.com/Guocork">@Guocork</a></li>
<li><a href="https://github.com/cassaundra">Cassaundra</a></li>
<li>My colleagues who provide invaluable guidance, technical advice, and community connections: Yue Chen, Edward Tan, Sid Askary, Yong He, Mats Lundgren</li>
<li>Linebender teammembers, for technical recommendations and serving as a sounding board for exchanging ideas</li>
<li><a href="https://github.com/smarizvi110">@smarizvi110</a> and other miscellaneous contributors from the open-source community</li>
</ul>
<hr style="border: none; width: 100%; color: #000000; background-color: #000000; height: 1px;" >
<!-- Links -->
Project Robius in 2024: one year of Rust App Dev2025-01-08T00:00:00+00:002025-01-08T00:00:00+00:00
Unknown
https://robius.rs/blog/robius-retrospective-2024/<p><em>Author: <a href="https://github.com/kevinaboos">Kevin Boos</a>. Last updated January 16th, 2025.</em> <a href="https://www.reddit.com/r/rust/comments/1i2wq5j/project_robius_in_2024_another_year_of_progress/">Discuss this on Reddit</a>.</p>
<p>This past year marked the first year of work on <strong>Project Robius</strong>: an open-source decentralized endeavor to enable developers to write immersive, fully-featured apps in pure 🦀 Rust 🦀 that work seamlessly across all major platforms.</p>
<blockquote>
<p><font style="bold" color="#C85911"> <strong>ℹ️ Project Robius in a nutshell</strong> </font></p>
<p>While GUIs are the foundation of an app, there is more to developing a modern featureful app than just drawing a UI.
Project Robius aims to fill in the gaps in the Rust app dev ecosystem by focusing on everything <em>except</em> the UI, such as abstractions for platform features & OS services, build & packaging tooling, and more.
We leave UI work to the experts behind the many excellent Rust UI toolkits under active development.</p>
</blockquote>
<p>In this post, we'll take a look back on what we've accomplished so far to make the world of Rust App Dev a little better:</p>
<ol>
<li>The crates we've published for accessing platform-provided features from Rust code</li>
<li>The major apps we've built using <a href="https://makepad.nl/">Makepad</a> + Robius together</li>
<li>The contributions we've made to existing open-source projects in the App Dev space</li>
<li>The connections we've fostered throughout the Rust community</li>
</ol>
<p>We'll also take a deeper look at <strong>Robrix</strong>, a multi-platform <a href="https://matrix.org/">Matrix</a> chat client written from scratch in Rust using the <a href="https://makepad.nl/">Makepad UI toolkit</a> and Robius components.</p>
<h2 id="1-robius-crates-for-platform-feature-abstractions">1. Robius crates for platform feature abstractions</h2>
<p>As our primary objective, we have published several crates intended to be used directly by app devs to access a given platform feature or OS service from their app. The main goal here is for each crate to offer a safe, platform-agnostic abstraction, such that the app dev need not worry about writing any platform-specific code or dealing with each platforms' idiosyncracies.</p>
<p>We began working on these crates in late Spring of 2024.
While development started out gradually, we are significantly ramping up our efforts for the coming year and expect to publish more frequently. That being said, here is the current list:</p>
<ul>
<li><a href="https://github.com/project-robius/robius-location"><code>robius-location</code></a>: access the current geolocation of the user's device</li>
<li><a href="https://crates.io/crates/robius-authentication"><code>robius-authentication</code></a>: display a native biometric or password authentication prompt</li>
<li><a href="https://crates.io/crates/robius-open"><code>robius-open</code></a>: open a URI or file in a different app (determined by the system)</li>
<li><a href="https://crates.io/crates/robius-directories"><code>robius-directories</code></a>: access platform-standard directory locations for app data, user data, config, cache, etc
<ul>
<li>a fork of the <a href="https://crates.io/crates/directories"><code>directories</code></a> crate that adds support for Android</li>
</ul>
</li>
<li><a href="https://github.com/project-robius/robius-url-handler"><code>robius-url-handler</code></a>: register your Rust app as the default handler for a URL scheme or file association</li>
<li><a href="https://github.com/project-robius/robius-keychain"><code>robius-keychain</code></a>: store and retrieve secure data/passwords from the platform's secure storage facility
<ul>
<li>This crate was written from scratch, but we discovered <a href="https://crates.io/crates/keyring"><code>keyring-rs</code></a> shortly after finishing it. We intend to contribute our additional features, mostly Android support, into <code>keyring-rs</code>.</li>
</ul>
</li>
<li><a href="https://github.com/project-robius/robius-file-dialog"><code>robius-dialog</code></a>: (WIP) display a native dialog for showing a message or allowing the user to pick a file, directory, image, etc.
<ul>
<li>This offers a custom implementation for iOS and Android, but uses <a href="https://crates.io/crates/rfd"><code>rfd</code></a> under the hood for Desktop platforms.</li>
</ul>
</li>
</ul>
<p>We've also released some "lower-level" crates that aren't intended for direct use by an app developer, but they'd be useful for other developers that want to create their own platform abstractions.
The above crates depend on these in various ways.</p>
<ul>
<li><a href="https://crates.io/crates/android-build"><code>android-build</code></a>: enables a Rust crate to automatically build Java code for Android targets, as part of a cargo build process via a <code>build.rs</code> build script.
<ul>
<li>The Java classfile(s) can then be used by your Rust app, typically via one of the above platform feature abstraction crates.</li>
</ul>
</li>
<li><a href="https://crates.io/crates/robius-android-env"><code>robius-android-env</code></a>: abstracts access to Android states owned by various UI toolkits.
<ul>
<li>Many Android platform features require passing in the current activity or accessing the JavaVM or JNI environment state.</li>
<li>This crate enables us to write other Rust crates that access Android platform features, such that they work seamlessly across many different UI toolkits.</li>
<li>Directly supports <a href="https://crates.io/crates/ndk-context"><code>ndk-context</code></a>, an existing crate which is compatible with many other UI toolkit components, e.g., [Winit]
<ul>
<li>This enables all of the above platform feature abstraction crates (plus any crate that depends on <code>robius-android-env</code>) to work with Winit-based apps on Android.</li>
</ul>
</li>
<li>Compared to <code>ndk-context</code>, the <code>robius-android-env</code> crate offers a "batteries-included" experience that automatically "just works" with supported UI toolkits, such that the app dev doesn't have to add any code to make things work.</li>
</ul>
</li>
</ul>
<h2 id="2-apps-built-in-2024-using-makepad-robius">2. Apps built in 2024 using Makepad + Robius</h2>
<p>We (with help from many collaborators) have built both small proof-of-concept demo apps and larger "flagship" apps using Makepad + Robius. Two of the most complex flagship apps we've been developing in 2024 are:</p>
<ul>
<li><a href="https://github.com/project-robius/robrix">Robrix</a>: a Matrix chat client for power users</li>
<li><a href="https://github.com/moxin-org/moly">Moly</a>: a local LLM chat runtime and AI agent explorer (previously "Moxin")</li>
</ul>
<p>Both of these apps are fully open-source and have releases available on their GitHub pages linked above, in case you'd like to download and try them out. Note that it's best to build them from source for the most up-to-date experience.</p>
<h3 id="robrix-an-up-and-coming-matrix-chat-client-for-power-users">Robrix: an up-and-coming Matrix chat client for power users</h3>
<p>We started <a href="https://github.com/project-robius/robrix">Robrix</a> about one year ago with the intention of it being a "flagship" Robius app — one that would help drive the development (and priority) of various Robius components and demonstrate their utility.
Since then, our plans for Robrix have expanded beyond it serving as just a demo app or a basic Matrix client; we discuss our longer-term, multi-stage <a href="../robius-roadmap-2025#robrix-roadmap-for-2025-and-beyond">vision for Robrix in the next post</a>.</p>
<p>Robrix has come a long way over the past year, thanks to 750+ commits from 10 contributors!
Since starting from scratch, we have created a functional Matrix chat client with most fundamental features already complete and working well, as shown by our feature status tracker below.</p>
<a href="/blog/robrix_feature_status_tracker.png">
<img style="width:98%" src="/blog/robrix_feature_status_tracker.png" alt="Robrix's feature status tracker">
</a>
<p>With these features in place, we have began dogfooding Robrix as a daily Matrix client!</p>
<p>While not all main features are complete, Robrix <em>does</em> already have some cool features that help both power users and casual users be more productive.
The biggest unique feature of Robrix is an "IDE-like" desktop UI that can display multiple rooms side-by-side in separate tabs, which can be docked and moved around via drag-n-drop actions.
No more wasted horizontal space!</p>
<a href="/blog/robrix_desktop_ui.png">
<img style="width:98%" src="/blog/robrix_desktop_ui.png" alt="Robrix side-by-side dockable tab UI">
</a>
<p>Another cool feature is that Robrix's UI can automatically transition to different view layouts based on window size. This enables our single codebase to run seamlessly on desktop and mobile platforms, but you can also use any view on any platform if you want.
For example, we frequently enjoy using the mid-size tablet view (below, left) or the narrow mobile view (below, middle) on a smaller laptop screen too, in addition to on our smartphones (below, right).</p>
<div style="gap: 50px;">
<a href="/blog/robrix_midsize_ui.png">
<img style="width: 43%" alt="Robrix mid-size UI view" src="/blog/robrix_midsize_ui.png" />
</a>
<a href="/blog/robrix_mobile_view_rooms_list.png">
<img style="width: 27.5%" alt="Robrix narrow mobile UI view of the rooms list" src="/blog/robrix_mobile_view_rooms_list.png" />
</a>
<a href="/blog/robrix_android_view_single_room.png">
<img style="width: 25.9%" alt="Robrix narrow mobile UI view on Android of a single room" src="/blog/robrix_android_view_single_room.png" />
</a>
</div>
<p>Beyond a sleek UI, Robrix also leverages multiple Robius crates for deep integration with the native platform:</p>
<ul>
<li><code>robius-open</code> to open URLs, images, and downloaded files</li>
<li><code>robius-location</code> to obtain and share the user's current location in a Matrix room</li>
<li><code>robius-url-handler</code> to register Robrix as a default handler for the <code>matrix:</code> URL scheme (and others)</li>
<li><code>robius-directories</code> to ensure that we store app data and cached content in the platform-canonical directories</li>
<li><code>robius-keychain</code> to store a user's login session tokens (this is a WIP)</li>
<li><code>robius-packaging-commands</code> to help easily build app bundles for desktop platforms using cargo-packager</li>
<li>In the future, we'll allow users to mark individual rooms as "secret", such that they are hidden behind an authentication prompt provided by <code>robius-authentication</code></li>
</ul>
<p>In addition to a sleek UI and robust platform integration, Robrix achieves high performance and efficiency thanks to its underlying pure-Rust stack and Makepad's emphasis on lightweight, performant code.
Robrix consistently hits the maximum 120 FPS on an older M1 Macbook Pro, remaining smooth and responsive even when scrolling through 10+ rooms displayed side-by-side.
We achieve this while using only around 26-30% of the system RAM that major Electron-based Matrix desktop clients consume to display a single room.
(Note: these are preliminary figures that require deeper benchmarking analysis before drawing conclusions from them.)</p>
<p>Most importantly, thanks to the power of Makepad and Robius, Robrix has zero platform-specific code.
This makes it easy to maintain and develop features/bugfixes quickly, as you don't have to consider the idiosyncracies of each platform.
Thus, we invite you to check out our codebase and contribute any cool features that you'd love to have!</p>
<p>To learn more about Robrix, check out the following resources:</p>
<ul>
<li><a href="https://github.com/project-robius/robrix">Robrix's GitHub repository</a></li>
<li><a href="https://www.youtube.com/watch?v=DO5C7aITVyU">A recent conference talk about Robrix</a> (<a href="https://github.com/project-robius/files/blob/main/GOSIM%20China%202024/Robrix%20Talk%20GOSIM%20China%20October%2017%2C%202024.pdf">PDF slides</a>)</li>
<li><a href="https://github.com/orgs/project-robius/projects/4/">Robrix's Project Tracker on GitHub</a></li>
<li><a href="https://matrix.to/#/#robius-robrix:matrix.org">Chat with us about Robrix on Matrix</a></li>
</ul>
<h3 id="moly-chat-with-local-llms-and-custom-ai-agents">Moly: chat with local LLMs and custom AI agents</h3>
<p><a href="https://github.com/moxin-org/moly">Moly</a> (f.k.a. <em>Moxin</em>) is a pure Rust GUI client for running local Large Language Models (LLMs) and chatting with various AI agents.
You can discover, browse, and download major open-source AI models:</p>
<a href="/blog/moly_discover_screen.png">
<img style="width:98%" src="/blog/moly_discover_screen.png" alt="Moly's discover LLM screen">
</a>
<p>and then chat with them <em>locally</em> without contacting any hosted LLM service.</p>
<a href="/blog/moly_chat_screen.png">
<img style="width:98%" src="/blog/moly_chat_screen.png" alt="Moly's LLM chat screen">
</a>
<p>Like Robrix, Moly was started about one year ago completely from scratch, and has been a significant driver for the development of fundamental Makepad widgets, components, and Robius infrastructure.
For example, Project Robius contributions to Moly and to other projects (at the request of Moly) were driven by these needs:</p>
<ul>
<li>Better packaging logic and build configuration, which became <a href="https://github.com/project-robius/robius-packaging-commands"><code>robius-packaging-commands</code></a>.
<ul>
<li>This cooperates with <code>cargo-packager</code> to generate Moly app bundles for all 3 major desktop platforms.</li>
</ul>
</li>
<li>Portable Rust installation & setup "scripts" that run before the GUI app starts, which became <a href="https://github.com/moxin-org/moly/blob/a82d297b155fa64efd2cdb5d6b14c89148a1c70b/moly-runner/src/main.rs"><code>moly-runner</code></a>.
<ul>
<li>This was needed to install and configure the <a href="https://wasmedge.org/">WasmEdge WASM runtime</a>, which is how Moly runs LLMs locally.</li>
<li>This is also useful for setting up the complex WasmEdge + Moly development environment in just one click.</li>
</ul>
</li>
<li>Many new Makepad widgets: modals, pop-up notifications, sliding panels, draggable sliders, etc.</li>
<li>Standardized app behaviors to be more platform-compliant and canonical, e.g., proper use of app data directories.</li>
</ul>
<p>To learn more about Moly, check out <a href="https://dev.to/zhanghandong/moly-an-open-source-llm-client-implemented-in-pure-rust-1hmd">this blog post</a> that demonstrates more cool features, screenshots, and examples of what you can do with Moly.
Due to constraints from the underlying WasmEdge runtime, Moly currently runs only on major desktop platforms (Linux, macOS, Windows), but support for iOS and Android is planned.</p>
<h2 id="3-select-contributions-to-other-rust-app-dev-projects">3. Select contributions to other Rust app dev projects</h2>
<p>In addition to creating, maintaining, and publishing our own crates for Rust app dev, we also strive to contribute to and improve existing crates that are already prominently used in the ecosystem.</p>
<ul>
<li>We began using and making contributions to <a href="https://crates.io/crates/cargo-packager"><code>cargo-packager</code></a>, a packaging solution for Rust apps on Desktop target platforms created and open-sourced by Crab-Nebula, the folks behind the excellent Tauri ecosystem
<ul>
<li><a href="https://github.com/crabnebula-dev/cargo-packager/pulls?q=author%3Akevinaboos">Our contributions</a> were mostly minor bugfixes and improvements to allow the packaging infrastructure to be configured more flexibly</li>
<li>As previously mentioned, we published <a href="https://github.com/project-robius/robius-packaging-commands"><code>robius-packaging-commands</code></a>, a companion to <code>cargo-packager</code> that makes it easier to build & configure complex apps
<ul>
<li>Automatically calculates the set of dependencies for Debian <code>.deb</code> packages</li>
<li>Automatically handles Makepad configuration and resource/asset discovery & bundling</li>
</ul>
</li>
<li>We intend to add support for other Desktop package formats, namely Flatpack</li>
<li>We also plan to contribute support for generating mobile app bundles, namely Android</li>
</ul>
</li>
<li>We have made <a href="https://github.com/makepad/makepad/pulls?q=author%3Akevinaboos">myriad major contributions</a> to the Makepad UI toolkit, as Robrix and Moly are two of the most complex/demanding apps built in Makepad
<ul>
<li>Improvements to <code>PortalList</code>, a virtual viewport list with infinite scrolling
<ul>
<li>Better API with more introspection into the positional & visibility state of items in the list, its scrolling state, and its item caching behavior</li>
<li>Efficient implementations of smooth scrolling animations, e.g., jump to bottom or jump to a given item index</li>
<li>Redesign how items are stored and indexed, and how visible items are tracked</li>
</ul>
</li>
<li>Rich text formatting for displaying both HTML and Markdown content
<ul>
<li>Including support for most formatting-relevant tags: (un)ordered lists, strikethrough/underline, coloring, indentation, blockquote, code, etc.</li>
<li>Special handling of interactive components like HTML links, which must preserve external formatting</li>
</ul>
</li>
<li>Multiple new widgets: avatar images with text fallback, abstractions over rich (HTML) text and plaintext, modals, sliding panes, etc</li>
<li>Make writing event handlers more ergonomic by avoiding mutable borrows when querying views/widgets</li>
<li>Redesign of underlying Android platform layer to allow external crates to access Android system states</li>
<li>Enable correct discovery of resource/asset files in macOS/iOS app bundles</li>
<li>Many improvements to <code>cargo-makepad</code>, a build tool to generate mobile app packages
<ul>
<li>Overhaul code to generate Android APKs</li>
<li>Properly install/configure the NDK toolchain on all 3 desktop platforms, plus enable building native code (via <code>cc-rs</code>)</li>
<li>Ensure backwards compatibility with standard Android Studio-managed SDKs</li>
</ul>
</li>
<li>An improved app lifecycle model with dedicated events for all lifecycle stages, which is consistent across all platforms</li>
<li>Easier and more ergonomic <code>Actions</code> (widget-to-widget message events)
<ul>
<li>Plus support for delivering an action to a widget from a background thread or async task context</li>
</ul>
</li>
</ul>
</li>
<li>We made <a href="https://github.com/kornelski/rust-security-framework/pull/210">minor contributions</a> to the <a href="https://crates.io/crates/security-framework"><code>security-framework</code></a> crate, which offers Rust bindings to Apple's security framework (for TLS, keychain, etc)
<ul>
<li>We added a few missing APIs to enabling updating or deleting keychain items, which we needed to fully implement <a href="https://github.com/project-robius/robius-keychain"><code>robius-keychain</code></a></li>
</ul>
</li>
<li>We implemented a Rust auto-installer and configurer for the <a href="https://wasmedge.org/">WasmEdge WASM runtime</a>, as mentioned <a href="https://robius.rs/blog/robius-retrospective-2024/#moly-chat-with-local-llms-and-custom-ai-agents">above</a>
<ul>
<li>This massively simplifies both the developer-side build process and the user installation procedure for Moly, which relies on WasmEdge to run LLMs locally.</li>
<li>We hope to transform this into the official install script for WasmEdge and upstream it for general usage there, as much of the effort involved was devoted to extracting the precise system configuration required to select and install the proper WasmEdge release.</li>
</ul>
</li>
</ul>
<h2 id="4-cross-collaboration-with-other-ui-and-app-dev-orgs">4. Cross-collaboration with other UI and App Dev orgs</h2>
<p>Beyond publishing crates and developing apps, we also want to bring together people of all stripes across the Ruist UI and App Dev ecosystem.
To that end, Project Robius hosted an <a href="https://2024.rustnl.org/unconf/">App Dev unconference</a> at RustNL 2024 (and also GOSIM Beijing 2024), in which a few dozen Rust developers from across the world met up to discuss the shared problems we all face in developing Rust apps and UI toolkits.
We discussed everything from build tooling to text layout, accessibility, Winit compatibility, and more.
A few of the topics & ideas from the unconference(s) have already made it past the discussion phase and have become real projects!</p>
<ul>
<li><a href="https://crates.io/crates/kittest"><code>kittest</code></a>: a universal UI testing framework built upon the <a href="https://accesskit.dev/">AccessKit</a> accessibility framework, spearheaded by the eGUI team!</li>
<li><a href="https://www.reddit.com/r/rust/comments/1ezdjqx/media_i_added_instant_hotreloading_of_some_rust/">Dioxus's work on hotreloading</a> not just UI DSL code, but even real Rust code that implements app behavior!</li>
<li>Feedback given to the Rust project teams, primarily lang, libs, and compiler.
<ul>
<li>We focused on changes to Rust that will make future Rust apps easier to write, with simplified and more ergonomic code patterns for async and more.</li>
</ul>
</li>
</ul>
<p>In addition, thanks to our colleague Sid Askary, we began monthly meet-ups to chat about ongoing Rust UI & App Dev concerns, and to share ideas, solutions, progress updates.
Attendees vary, but often include teammembers from <a href="https://robius.rs/">Robius</a>, <a href="https://makepad.nl/">Makepad</a>, the <a href="https://linebender.org/">Linebender organization</a> (behind Xilem and more), <a href="https://dioxuslabs.com/">Dioxus</a>, <a href="https://github.com/emilk/egui">eGUI</a>, <a href="https://www.pax.dev/">Pax</a>, <a href="https://github.com/gfx-rs/wgpu">wgpu</a>, <a href="https://slint.dev/">Slint</a>, and more.
If you're in the Rust App Dev or UI space and would like to join future meetups, consider getting in touch!</p>
<h2 id="our-roadmaps-for-2025">Our Roadmaps for 2025</h2>
<p><a href="../robius-roadmap-2025">Check out our next blog post</a> for roadmaps for both Project Robius and Robrix in 2025 (and beyond).</p>
<h2 id="acknowledgments">Acknowledgments</h2>
<p>If you made it this far, thanks for reading! You must be a true fan of Rust app dev 😊!</p>
<p>Before we depart, I'd like to thank the following key people who have been instrumental to the success of Project Robius over the past year.</p>
<ul>
<li>The Makepad team: <a href="https://x.com/rikarends">Rik Arends</a>, <a href="https://github.com/ejpbruel2">Eddy Bruël</a>, <a href="https://twitter.com/SebMichailidis">Sebastian Michailidis</a></li>
<li><a href="https://github.com/tsoutsman">Klim Tsoutsman</a></li>
<li><a href="https://www.wyeworks.com/">WyeWorks</a> developers: <a href="https://github.com/jmbejar">Jorge Bejar</a>, <a href="https://github.com/joulei">Julián Montes de Oca</a>, <a href="https://github.com/fmzbl">Facundo Mendizábal</a></li>
<li><a href="https://github.com/ZhangHanDong">Alex Zhang (ZhangHanDong)</a> and his team members: <a href="https://github.com/alanpoon">@alanpoon</a>, <a href="https://github.com/aaravlu">@aaravlu</a>, <a href="https://github.com/tyreseluo">@tyreseluo</a>, <a href="https://github.com/Guocork">@Guocork</a></li>
<li><a href="https://github.com/cassaundra">Cassaundra</a></li>
<li>My colleagues who provide invaluable guidance, technical advice, and community connections: Yue Chen, Edward Tan, Sid Askary, Yong He, Mats Lundgren</li>
<li>Linebender teammembers, for technical recommendations and serving as a sounding board for exchanging ideas</li>
<li><a href="https://github.com/smarizvi110">@smarizvi110</a> and other miscellaneous contributors from the open-source community</li>
</ul>
<hr style="border: none; width: 100%; color: #000000; background-color: #000000; height: 1px;" >
<!-- Links -->
Building a ChatGPT client using Rust with Makepad2024-06-12T00:00:00+00:002024-06-12T00:00:00+00:00
Unknown
https://robius.rs/blog/building-chatgpt-client-using-makepad/<p>The Rust ecosystem is making significant strides in developing first-class tools for building production-ready applications compatible with major platforms. While there's still work to be done to match the developer experience offered by other tech stacks, we can already explore our options. You'll be impressed by what Rust can accomplish today.</p>
<p>This is the first post in a series that explores how to create cross-platform applications with <a href="https://github.com/makepad/makepad">Makepad</a>, a notable application framework in the Rust community. It is also <a href="https://project-robius.github.io/book/#key-community-projects">one of the key projects under the Robuis initiative</a>. Although it's still evolving and some aspects are being fine-tuned for general production readiness, we can now create impressive applications in just a few steps.</p>
<h2 id="create-a-new-makepad-application">Create a new Makepad application</h2>
<p>Let’s dive into it! First, create a new binary project using <code>cargo</code>:</p>
<pre data-lang="sh" style="background-color:#eff1f5;color:#4f5b66;" class="language-sh "><code class="language-sh" data-lang="sh"><span style="color:#bf616a;">cargo</span><span> new mychat
</span><span style="color:#96b5b4;">cd</span><span> mychat/
</span></code></pre>
<p>Now, we want to add <code>makepad</code> as a dependency.</p>
<pre data-lang="sh" style="background-color:#eff1f5;color:#4f5b66;" class="language-sh "><code class="language-sh" data-lang="sh"><span style="color:#bf616a;">cargo</span><span> add makepad-widgets</span><span style="color:#bf616a;"> --git</span><span> https://github.com/makepad/makepad</span><span style="color:#bf616a;"> --branch</span><span> rik
</span></code></pre>
<p>We are now ready to create the necessary elements for an empty application to run. This requires modifying the existing main.rs and adding a few lines.</p>
<p>The <code>src/main.rs</code> file should contain the following:</p>
<pre data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">main</span><span>() {
</span><span> mychat::app::app_main()
</span><span>}
</span></code></pre>
<p>And the <code>src/lib.rs</code> file:</p>
<pre data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">pub mod </span><span>app;
</span></code></pre>
<p>The <code>src/apps.rs</code> file is the actual entrypoint for Makepad applications. Here is our first version for this file:</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>1</td><td><span style="color:#b48ead;">use </span><span>makepad_widgets::*;
</span></td></tr><tr><td>2</td><td><span>
</span></td></tr><tr><td>3</td><td><span>live_design! {
</span></td></tr><tr><td>4</td><td><span> import makepad_widgets::base::*;
</span></td></tr><tr><td>5</td><td><span> import makepad_widgets::theme_desktop_dark::*;
</span></td></tr><tr><td>6</td><td><span>
</span></td></tr><tr><td>7</td><td><span> App = {{App}} {
</span></td></tr><tr><td>8</td><td><span> ui: <Window> {
</span></td></tr><tr><td>9</td><td><span> window: {inner_size: </span><span style="color:#96b5b4;">vec2</span><span>(</span><span style="color:#d08770;">800</span><span>, </span><span style="color:#d08770;">600</span><span>)},
</span></td></tr><tr><td>10</td><td><span> pass: {clear_color: #</span><span style="color:#d08770;">000</span><span>}
</span></td></tr><tr><td>11</td><td><span> }
</span></td></tr><tr><td>12</td><td><span> }
</span></td></tr><tr><td>13</td><td><span>}
</span></td></tr><tr><td>14</td><td><span>
</span></td></tr><tr><td>15</td><td><span>app_main!(App);
</span></td></tr><tr><td>16</td><td><span>
</span></td></tr><tr><td>17</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(Live, LiveHook)]
</span></td></tr><tr><td>18</td><td><span style="color:#b48ead;">pub struct </span><span>App {
</span></td></tr><tr><td>19</td><td><span> #[</span><span style="color:#bf616a;">live</span><span>]
</span></td></tr><tr><td>20</td><td><span> </span><span style="color:#bf616a;">ui</span><span>: WidgetRef,
</span></td></tr><tr><td>21</td><td><span>}
</span></td></tr><tr><td>22</td><td><span>
</span></td></tr><tr><td>23</td><td><span style="color:#b48ead;">impl </span><span>LiveRegister </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>24</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">live_register</span><span>(</span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx) {
</span></td></tr><tr><td>25</td><td><span> makepad_widgets::live_design(cx);
</span></td></tr><tr><td>26</td><td><span> }
</span></td></tr><tr><td>27</td><td><span>}
</span></td></tr><tr><td>28</td><td><span>
</span></td></tr><tr><td>29</td><td><span style="color:#b48ead;">impl </span><span>AppMain </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>30</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_event</span><span>(&</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>, </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">event</span><span>: &Event) {
</span></td></tr><tr><td>31</td><td><span> </span><span style="color:#b48ead;">let</span><span> scope = &</span><span style="color:#b48ead;">mut </span><span>Scope::empty();
</span></td></tr><tr><td>32</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">handle_event</span><span>(cx, event, scope);
</span></td></tr><tr><td>33</td><td><span> }
</span></td></tr><tr><td>34</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>You can test our new application by executing the cargo run command. For now, you should see an empty window.</p>
<h2 id="understanding-our-application-s-anatomy">Understanding our application's anatomy</h2>
<p>Looking at the code in <code>src/app.rs</code>, we can see various sections. One section includes a call to the <code>live_design!</code> macro, provided by Makepad. This is where we define the UI components and layout of our application.</p>
<p>Defining a top-level block named <code>App</code> is essential. The behavior of this <code>App</code> element, which represents the entire application, is determined by the Rust struct <code>App</code>. We'll delve into this shortly. Note that our application has only one <code>Window</code> widget instance in the <code>App</code> definition, representing the "empty window" you see when running the application.</p>
<p>So, how would we go about displaying a "Hello world!" message? It's simply a matter of adding a <code>Label</code> widget instance inside the <code>Window</code> block.</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>3</td><td><span>live_design! {
</span></td></tr><tr><td>6</td><td><span>
</span></td></tr><tr><td>7</td><td><span> App = {{App}} {
</span></td></tr><tr><td>8</td><td><span> ui: <Window> {
</span></td></tr><tr><td>9</td><td><span> window: {inner_size: </span><span style="color:#96b5b4;">vec2</span><span>(</span><span style="color:#d08770;">800</span><span>, </span><span style="color:#d08770;">600</span><span>)},
</span></td></tr><tr><td>10</td><td><span> pass: {clear_color: #</span><span style="color:#d08770;">000</span><span>}
</span></td></tr><tr><td>11</td><td><span>
</span></td></tr><tr><td>12</td><td><span> </span><span style="color:#a7adba;">// Adding a label displaying some text
</span></td></tr><tr><td>13</td><td><span> body = {
</span></td></tr><tr><td>14</td><td><span> <Label> {
</span></td></tr><tr><td>15</td><td><span> margin: </span><span style="color:#d08770;">40</span><span>,
</span></td></tr><tr><td>16</td><td><span> text: "</span><span style="color:#a3be8c;">Hello World!</span><span>"
</span></td></tr><tr><td>17</td><td><span> draw_text: {
</span></td></tr><tr><td>18</td><td><span> color: #</span><span style="color:#d08770;">000</span><span>,
</span></td></tr><tr><td>19</td><td><span> }
</span></td></tr><tr><td>20</td><td><span> }
</span></td></tr><tr><td>21</td><td><span> }
</span></td></tr><tr><td>22</td><td><span> }
</span></td></tr><tr><td>23</td><td><span> }
</span></td></tr><tr><td>24</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>If you modified the application while it was running, you may have noticed that the changes were immediately reflected. This is thanks to Makepad's built-in Live Design feature which automatically detects UI related changes and "hot reloads" the GUI without any recompilation.</p>
<p>All UI elements should be defined within a block named <code>body</code>, which is specified in the <code>Window</code> widget.</p>
<p>Let's take a deeper look at the Rust code section of the <code>src/app.rs</code> file:</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>56</td><td><span>app_main!(App);
</span></td></tr><tr><td>57</td><td><span>
</span></td></tr><tr><td>58</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(Live, LiveHook)]
</span></td></tr><tr><td>59</td><td><span style="color:#b48ead;">pub struct </span><span>App {
</span></td></tr><tr><td>60</td><td><span> #[</span><span style="color:#bf616a;">live</span><span>]
</span></td></tr><tr><td>61</td><td><span> </span><span style="color:#bf616a;">ui</span><span>: WidgetRef,
</span></td></tr><tr><td>62</td><td><span>}
</span></td></tr><tr><td>63</td><td><span>
</span></td></tr><tr><td>64</td><td><span style="color:#b48ead;">impl </span><span>LiveRegister </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>65</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">live_register</span><span>(</span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx) {
</span></td></tr><tr><td>66</td><td><span> makepad_widgets::live_design(cx);
</span></td></tr><tr><td>67</td><td><span> }
</span></td></tr><tr><td>68</td><td><span>}
</span></td></tr><tr><td>69</td><td><span>
</span></td></tr><tr><td>70</td><td><span style="color:#b48ead;">impl </span><span>AppMain </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>71</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_event</span><span>(&</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>, </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">event</span><span>: &Event) {
</span></td></tr><tr><td>72</td><td><span> </span><span style="color:#b48ead;">let</span><span> scope = &</span><span style="color:#b48ead;">mut </span><span>Scope::empty();
</span></td></tr><tr><td>73</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">handle_event</span><span>(cx, event, scope);
</span></td></tr><tr><td>74</td><td><span> }
</span></td></tr><tr><td>75</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>The Rust code in this file is connected to the application's live UI code via the <code>app_main</code> macro (line 56). Makepad then recognizes the <code>App</code> struct as the one representing your application, and everything is tied together.</p>
<p>The <code>LiveRegister</code> trait must be implemented (lines 64-68) because the framework needs to know the locations of other <code>live_design</code> blocks to load them. Currently, we're only including the <code>live_design</code> block included in <code>makepad-widgets</code>, giving us access to framework-provided widgets like <code>Window</code> and <code>Label</code>. However, as your project expands, you'll likely define other parts of your application in different files.</p>
<p>Finally, the <code>AppMain</code> trait must also be implemented, at least minimally, as we've done (see lines 70-75). For now, we've implemented the <code>handle_event</code> function, which describes what happens when the user interacts with the application. The line <code>self.ui.handle_event(cx, event, scope);</code> is important as it invokes the <code>handle_event</code> handler function in the internal widget instances, such as our label instance.</p>
<p>In makepad, if you implement a <code>handle_event()</code> function, you effectively take control over event handling and propagation. Thus, you must explicitly pass events down into each subwidget (or "child" widget) within a widget, if you want each subwidget to be aware of the event and have the ability to handle or respond to it.
This gives you ultimate power over how events propagate throughout different UI widgets/components in the application.</p>
<blockquote>
<p>A crucial aspect to note in Makepad is the existence of a Draw event. This event is triggered when elements on the screen need rendering. If we fail to pass all events to child elements, they will not display because this event will not reach them.</p>
</blockquote>
<h2 id="building-the-user-interface-for-our-chat">Building the user interface for our chat</h2>
<p>We aim to create a basic version of a ChatGPT client, so let's begin by designing a simple interface. It should include a text input field for the user's prompt, a submit button to send the input to the ChatGPT API, and a list of messages to display the conversation.</p>
<p>For simplicity, we'll implement these features directly in our existing <a href="http://app.rs/"><code>app.rs</code></a> file. In reality, larger applications would distribute different parts across multiple files. We'll cover how to organize larger applications effectively in future posts.</p>
<p>Let's start by defining a general layout and adding the text input field and submit button.</p>
<pre data-linenos data-lang="bash" style="background-color:#eff1f5;color:#4f5b66;" class="language-bash "><code class="language-bash" data-lang="bash"><table><tbody><tr><td>2</td><td><span style="color:#bf616a;">live_design! </span><span>{
</span></td></tr><tr><td>5</td><td><span>
</span></td></tr><tr><td>6</td><td><span> App = {{App}} {
</span></td></tr><tr><td>7</td><td><span> ui: <Window> {
</span></td></tr><tr><td>10</td><td><span>
</span></td></tr><tr><td>11</td><td><span> body = {
</span></td></tr><tr><td>12</td><td><span> height: Fill,
</span></td></tr><tr><td>13</td><td><span> width: Fill,
</span></td></tr><tr><td>14</td><td><span> margin: {top: 40, bottom: 40, left: 100, right: 100},
</span></td></tr><tr><td>15</td><td><span>
</span></td></tr><tr><td>16</td><td><span> show_bg: true,
</span></td></tr><tr><td>17</td><td><span> draw_bg: {
</span></td></tr><tr><td>18</td><td><span> color: </span><span style="color:#a7adba;">#330
</span></td></tr><tr><td>19</td><td><span> }
</span></td></tr><tr><td>20</td><td><span>
</span></td></tr><tr><td>21</td><td><span> flow: Down,
</span></td></tr><tr><td>22</td><td><span> spacing: 20,
</span></td></tr><tr><td>23</td><td><span>
</span></td></tr><tr><td>24</td><td><span> messages = <View> {
</span></td></tr><tr><td>25</td><td><span> height: Fill,
</span></td></tr><tr><td>26</td><td><span> width: Fill,
</span></td></tr><tr><td>27</td><td><span> margin: 20,
</span></td></tr><tr><td>28</td><td><span> }
</span></td></tr><tr><td>29</td><td><span>
</span></td></tr><tr><td>30</td><td><span> prompt = <View> {
</span></td></tr><tr><td>31</td><td><span> height: Fit,
</span></td></tr><tr><td>32</td><td><span> width: Fill,
</span></td></tr><tr><td>33</td><td><span> margin: 20,
</span></td></tr><tr><td>34</td><td><span> spacing: 10,
</span></td></tr><tr><td>35</td><td><span>
</span></td></tr><tr><td>36</td><td><span> prompt_input = <TextInput> {
</span></td></tr><tr><td>37</td><td><span> height: Fit,
</span></td></tr><tr><td>38</td><td><span> width: Fill,
</span></td></tr><tr><td>39</td><td><span> padding: 10,
</span></td></tr><tr><td>40</td><td><span> empty_message: "</span><span style="color:#a3be8c;">Type a message...</span><span>",
</span></td></tr><tr><td>41</td><td><span> }
</span></td></tr><tr><td>42</td><td><span>
</span></td></tr><tr><td>43</td><td><span> send_button = <Button> {
</span></td></tr><tr><td>44</td><td><span> height: Fit,
</span></td></tr><tr><td>45</td><td><span> width: Fit,
</span></td></tr><tr><td>46</td><td><span> padding: 10,
</span></td></tr><tr><td>47</td><td><span> text: "</span><span style="color:#a3be8c;">Send</span><span>",
</span></td></tr><tr><td>48</td><td><span> }
</span></td></tr><tr><td>49</td><td><span> }
</span></td></tr><tr><td>50</td><td><span> }
</span></td></tr><tr><td>51</td><td><span> }
</span></td></tr><tr><td>52</td><td><span> }
</span></td></tr><tr><td>53</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>We have replaced all our body block. If you run the application you should see the following:</p>
<p><img src="/blog/makepad-chatgpt-first-run.png" alt="Chat interface" /></p>
<p>We arranged our layout using one of the Makepad's fundamental building blocks: the <code>View</code> widget. One view can have a list of children elements to render in our interface. In our case, we added two nested views and configured the parent to have them vertically organized (indicated by <code>flow: Down</code> in line 21).</p>
<p>Those two children views are identified as <code>messages</code> (line 24) and <code>prompt</code> (line 30). Though we're not using those identifiers yet, they will be necessary for reference in the Rust code later. Observe that the first one uses <code>height: Fill</code> and the second <code>height: Fit</code>. This succinctly conveys that the messages section should take up all available vertical space, with each message taking only the minimum amount of vertical space required to fit the message content in., excluding the area required for the <code>prompt</code> view. The <code>prompt</code> view's size relies solely on its inner content.</p>
<blockquote>
<p>A common source of issues when working with Makepad is when you have a view sized with <code>Fit</code>, but the inner content uses <code>Fill</code>. This can cause something to not be displayed at all. When a widget uses <code>Fill</code>, it needs to know the parent's size beforehand to calculate its own size. Conversely, when a widget is sized with <code>Fit</code>, it needs to calculate the space of its content, which must be calculated without knowing the parent's size.</p>
</blockquote>
<p>Here's an alternate way to organize our <code>live_design</code> code:</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>2</td><td><span>live_design! {
</span></td></tr><tr><td>5</td><td><span>
</span></td></tr><tr><td>6</td><td><span> Messages = <View> {
</span></td></tr><tr><td>7</td><td><span> </span><span style="color:#a7adba;">// Empty for now
</span></td></tr><tr><td>8</td><td><span> }
</span></td></tr><tr><td>9</td><td><span>
</span></td></tr><tr><td>10</td><td><span> Prompt = <View> {
</span></td></tr><tr><td>11</td><td><span> spacing: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>12</td><td><span>
</span></td></tr><tr><td>13</td><td><span> <TextInput> {
</span></td></tr><tr><td>14</td><td><span> height: Fit,
</span></td></tr><tr><td>15</td><td><span> width: Fill,
</span></td></tr><tr><td>16</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>17</td><td><span> empty_message: "</span><span style="color:#a3be8c;">Type a message...</span><span>",
</span></td></tr><tr><td>18</td><td><span> }
</span></td></tr><tr><td>19</td><td><span>
</span></td></tr><tr><td>20</td><td><span> <Button> {
</span></td></tr><tr><td>21</td><td><span> height: Fit,
</span></td></tr><tr><td>22</td><td><span> width: Fit,
</span></td></tr><tr><td>23</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>24</td><td><span> text: "</span><span style="color:#a3be8c;">Send</span><span>",
</span></td></tr><tr><td>25</td><td><span> }
</span></td></tr><tr><td>26</td><td><span> }
</span></td></tr><tr><td>27</td><td><span>
</span></td></tr><tr><td>28</td><td><span> App = {{App}} {
</span></td></tr><tr><td>29</td><td><span> ui: <Window> {
</span></td></tr><tr><td>32</td><td><span>
</span></td></tr><tr><td>33</td><td><span> body = {
</span></td></tr><tr><td>45</td><td><span>
</span></td></tr><tr><td>46</td><td><span> messages = <Messages> {
</span></td></tr><tr><td>47</td><td><span> height: Fill,
</span></td></tr><tr><td>48</td><td><span> width: Fill,
</span></td></tr><tr><td>49</td><td><span> margin: </span><span style="color:#d08770;">20</span><span>,
</span></td></tr><tr><td>50</td><td><span> }
</span></td></tr><tr><td>51</td><td><span> prompt = <Prompt> {
</span></td></tr><tr><td>52</td><td><span> height: Fit,
</span></td></tr><tr><td>53</td><td><span> width: Fill,
</span></td></tr><tr><td>54</td><td><span> margin: </span><span style="color:#d08770;">20</span><span>,
</span></td></tr><tr><td>55</td><td><span> }
</span></td></tr><tr><td>56</td><td><span> }
</span></td></tr><tr><td>57</td><td><span> }
</span></td></tr><tr><td>58</td><td><span> }
</span></td></tr><tr><td>59</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>This approach makes our <code>App</code> easier to read at first glance. These are specific parts of our user interface that have been assigned the aliases <code>Messages</code> (lines 6-8) and <code>Prompt</code>(10-26) and can be referred to from other points in the DSL. See into the <code>body</code> definitions and check how <code>Messages</code> (line 46) and <code>Prompt</code> (line 51) are instantiated.</p>
<p>We've opted to extract almost everything into these named UI blocks. However, it's worth noting that we still define (or override) the <code>height</code>, <code>width</code>, and <code>margin</code> where we use <code>Messages</code> and <code>Prompt</code> (see lines 46-48 and 52-54). This is because these values form part of the layout rules we establish in conjunction with the parent view. But, Makepad is highly flexible, allowing you to override as much content as desired to accommodate your needs in various ways.</p>
<h2 id="implementing-the-interaction">Implementing the interaction</h2>
<p>Currently, our application is limited to displaying user interface elements. Interaction is restricted to the text input, with no response when you click the <code>Send</code> button. Let's enhance this interface.</p>
<p>We need to make some changes to our UI code. We'll add some labels to the <code>Message</code> widget to display responses. We'll also add element identifiers, allowing us to reference them from Rust code later. Check the highlighted lines to spot the elements with identifiers.</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>2</td><td><span>live_design! {
</span></td></tr><tr><td>5</td><td><span>
</span></td></tr><tr><td>6</td><td><span> Messages = <View> {
</span></td></tr><tr><td>7</td><td><span> height: Fill,
</span></td></tr><tr><td>8</td><td><span> width: Fill,
</span></td></tr><tr><td>9</td><td><span> padding: </span><span style="color:#d08770;">20</span><span>,
</span></td></tr><tr><td>10</td><td><span>
</span></td></tr><tr><td>11</td><td><span> flow: Down,
</span></td></tr><tr><td>12</td><td><span> spacing: </span><span style="color:#d08770;">20</span><span>,
</span></td></tr><tr><td>13</td><td><span>
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">14</mark></td><td><mark style="background-color:#a7adba30;"><span> user_message_bubble = <RoundedView> {
</span></mark></td></tr><tr><td>15</td><td><span> visible: </span><span style="color:#d08770;">false</span><span>,
</span></td></tr><tr><td>16</td><td><span>
</span></td></tr><tr><td>17</td><td><span> height: Fit,
</span></td></tr><tr><td>18</td><td><span> width: Fill,
</span></td></tr><tr><td>19</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>20</td><td><span> draw_bg: {
</span></td></tr><tr><td>21</td><td><span> color: #</span><span style="color:#d08770;">222
</span></td></tr><tr><td>22</td><td><span> }
</span></td></tr><tr><td>23</td><td><span> user_message = <Label> {
</span></td></tr><tr><td>24</td><td><span> height: Fit,
</span></td></tr><tr><td>25</td><td><span> width: Fill,
</span></td></tr><tr><td>26</td><td><span> }
</span></td></tr><tr><td>27</td><td><span> }
</span></td></tr><tr><td>28</td><td><span>
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">29</mark></td><td><mark style="background-color:#a7adba30;"><span> model_message_bubble = <RoundedView> {
</span></mark></td></tr><tr><td>30</td><td><span> visible: </span><span style="color:#d08770;">false</span><span>,
</span></td></tr><tr><td>31</td><td><span>
</span></td></tr><tr><td>32</td><td><span> height: Fit,
</span></td></tr><tr><td>33</td><td><span> width: Fill,
</span></td></tr><tr><td>34</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>35</td><td><span> draw_bg: {
</span></td></tr><tr><td>36</td><td><span> color: #</span><span style="color:#d08770;">222
</span></td></tr><tr><td>37</td><td><span> }
</span></td></tr><tr><td>38</td><td><span> model_message = <Label> {
</span></td></tr><tr><td>39</td><td><span> height: Fit,
</span></td></tr><tr><td>40</td><td><span> width: Fill,
</span></td></tr><tr><td>41</td><td><span> }
</span></td></tr><tr><td>42</td><td><span> }
</span></td></tr><tr><td>43</td><td><span> }
</span></td></tr><tr><td>44</td><td><span>
</span></td></tr><tr><td>45</td><td><span> Prompt = <View> {
</span></td></tr><tr><td>46</td><td><span> height: Fit,
</span></td></tr><tr><td>47</td><td><span> width: Fill,
</span></td></tr><tr><td>48</td><td><span> margin: </span><span style="color:#d08770;">20</span><span>,
</span></td></tr><tr><td>49</td><td><span> spacing: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>50</td><td><span>
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">51</mark></td><td><mark style="background-color:#a7adba30;"><span> message_input = <TextInput> {
</span></mark></td></tr><tr><td>52</td><td><span> height: Fit,
</span></td></tr><tr><td>53</td><td><span> width: Fill,
</span></td></tr><tr><td>54</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>55</td><td><span> empty_message: "</span><span style="color:#a3be8c;">Type a message...</span><span>",
</span></td></tr><tr><td>56</td><td><span> }
</span></td></tr><tr><td>57</td><td><span>
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">58</mark></td><td><mark style="background-color:#a7adba30;"><span> send_button = <Button> {
</span></mark></td></tr><tr><td>59</td><td><span> height: Fit,
</span></td></tr><tr><td>60</td><td><span> width: Fit,
</span></td></tr><tr><td>61</td><td><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span></td></tr><tr><td>62</td><td><span> text: "</span><span style="color:#a3be8c;">Send</span><span>",
</span></td></tr><tr><td>63</td><td><span> }
</span></td></tr><tr><td>64</td><td><span> }
</span></td></tr><tr><td>96</td><td><span>
</span></td></tr><tr><td>98</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>Note we are using <code>visible: false</code> in some views to hide the messages bubbles (lines 15 and 30). We plan to toggle the visibility once we have some messages to display.</p>
<p>Now we can add the Rust logic to implement the “send button”.</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>114</td><td><span style="color:#b48ead;">impl </span><span>AppMain </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>115</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_event</span><span>(&</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>, </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">event</span><span>: &Event) {
</span></td></tr><tr><td>116</td><td><span> </span><span style="color:#a7adba;">// Added this line
</span></td></tr><tr><td>117</td><td><span> </span><span style="color:#bf616a;">self</span><span>.</span><span style="color:#96b5b4;">match_event</span><span>(cx, event);
</span></td></tr><tr><td>118</td><td><span>
</span></td></tr><tr><td>119</td><td><span> </span><span style="color:#b48ead;">let</span><span> scope = &</span><span style="color:#b48ead;">mut </span><span>Scope::empty();
</span></td></tr><tr><td>120</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">handle_event</span><span>(cx, event, scope);
</span></td></tr><tr><td>121</td><td><span> }
</span></td></tr><tr><td>122</td><td><span>}
</span></td></tr><tr><td>123</td><td><span>
</span></td></tr><tr><td>124</td><td><span style="color:#b48ead;">impl </span><span>MatchEvent </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>125</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_actions</span><span>(&</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>, </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">actions</span><span>:&Actions){
</span></td></tr><tr><td>126</td><td><span> </span><span style="color:#b48ead;">if </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">button</span><span>(id!(send_button)).</span><span style="color:#96b5b4;">clicked</span><span>(&actions) {
</span></td></tr><tr><td>127</td><td><span> </span><span style="color:#a7adba;">// Capture the text input value
</span></td></tr><tr><td>128</td><td><span> </span><span style="color:#b48ead;">let</span><span> user_prompt = </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">text_input</span><span>(id!(message_input)).</span><span style="color:#96b5b4;">text</span><span>();
</span></td></tr><tr><td>129</td><td><span>
</span></td></tr><tr><td>130</td><td><span> </span><span style="color:#a7adba;">// Set the text of the user message label
</span></td></tr><tr><td>131</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">label</span><span>(id!(user_message)).</span><span style="color:#96b5b4;">set_text</span><span>(&user_prompt);
</span></td></tr><tr><td>132</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">view</span><span>(id!(user_message_bubble)).</span><span style="color:#96b5b4;">set_visible</span><span>(</span><span style="color:#d08770;">true</span><span>);
</span></td></tr><tr><td>133</td><td><span>
</span></td></tr><tr><td>134</td><td><span> </span><span style="color:#a7adba;">// Simulate a model response
</span></td></tr><tr><td>135</td><td><span> </span><span style="color:#b48ead;">let</span><span> model_response = "</span><span style="color:#a3be8c;">Hello, I am a model response!</span><span>";
</span></td></tr><tr><td>136</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">label</span><span>(id!(model_message)).</span><span style="color:#96b5b4;">set_text</span><span>(model_response);
</span></td></tr><tr><td>137</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">view</span><span>(id!(model_message_bubble)).</span><span style="color:#96b5b4;">set_visible</span><span>(</span><span style="color:#d08770;">true</span><span>);
</span></td></tr><tr><td>138</td><td><span>
</span></td></tr><tr><td>139</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">redraw</span><span>(cx);
</span></td></tr><tr><td>140</td><td><span> }
</span></td></tr><tr><td>141</td><td><span> }
</span></td></tr><tr><td>142</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>Let's examine each of these code blocks in detail. First, we added a call to <code>self.match_event(cx, event)</code> in line 117, which allows us to use the simpler form of Event matching/handling. This requires us to implement the <code>MatchEvent</code> trait for the <code>App</code> struct, which we can then use to check for and handle events like a <code>Button</code> click.</p>
<p>Next, we implement <code>handle_actions</code> to handle the <em>clicked action</em> emitted by the button when the user clicks it (see lines 125-141). The key idea in Makepad is that widgets consume <em>events</em> from other sources and then can emit <em>actions</em> as needed to communicate with other widgets. In this case, the button instance already received and handled a click event and has emitted a related <em>clicked action</em>.
We rely on the <code>clicked(&actions)</code> function (line 126) to check if the received actions were actually emited by this button instance.</p>
<p>You may notice how we are relying on the identifiers we have in our <code>live_design</code> counterpart. Things like <code>self.ui.text_input(id!(message_input))</code>, <code>self.ui.label(id!(user_message))</code> and <code>self.ui.view(id!(model_message_bubble))</code>. This is the Makepad query system for UI elements from Rust code, which is very confortable to use. Just remember you need to use the appropriate function depending on the widget type you’re looking for. In other words, <code>self.ui.view(id!(message_input))</code> won’t return anything because the <code>message_input</code> id was used for a <code>TextInput</code> rather than a <code>View</code>.</p>
<p>As a final note, Makepad has a drawing mode which is quite explicit. So, it is our call to indicate to the framework that there were changes in the labels and views instances that needs to be redraw. Hence, we have an invocation to do it: <code>self.ui.redraw(cx)</code>, in the line 139. This is a very simple way to “redraw everything” that is not the most efficient way if you have a much more elaborated UI where only a tiny portion has changed, but it is probably a good way to start for now. Nothing stops you to try later to invoke <code>redraw</code> in the individual instances of <code>Label</code>, <code>View</code> and <code>TextInput</code> as necessary.</p>
<p>Since we have changed Rust code we are required to recompile our application and run it again to see the changes. Hopefully, you will notice how fast Makepad applications recompile! This is a luxury to have in the Rust ecosystem thanks to the amount of care the Makepad team puts on it.</p>
<p><img src="/blog/makepad-chatgpt-first-interaction.png" alt="First chat interaction" /></p>
<p>It is working! We need to have a model delivering smarter responses now 🙂</p>
<h2 id="chatgpt-interaction">ChatGPT interaction</h2>
<p>It’s time to bring real conversation content to our app. We’re going to use the ChatGPT public API, so you will need to generate a key by signing into <a href="http://platform.openai.com">platform.openai.com</a>. Note the number of allowed requests is based on your <a href="https://platform.openai.com/docs/guides/rate-limits/usage-tiers">current usage tier</a>. Using the free tier, you may have to wait a bit while testing because you only get 3 requests per minute. In any case, our implementation will catch error responses and display them so the user always knows what's going on.</p>
<p>Once you have your API key, let’s implement the request to obtain a model response, by following the <a href="https://platform.openai.com/docs/api-reference/chat/create">official documentation</a>.</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>1</td><td><span style="color:#b48ead;">use </span><span>makepad_widgets::*;
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">2</mark></td><td><mark style="background-color:#a7adba30;"><span style="color:#b48ead;">use </span><span>makepad_micro_serde::*;
</span></mark></td></tr><tr><td>3</td><td><span>
</span></td></tr><tr><td>125</td><td><span style="color:#b48ead;">impl </span><span>MatchEvent </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>126</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_actions</span><span>(&</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>, </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">actions</span><span>:&Actions){
</span></td></tr><tr><td>127</td><td><span> </span><span style="color:#b48ead;">if </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">button</span><span>(id!(send_button)).</span><span style="color:#96b5b4;">clicked</span><span>(&actions) {
</span></td></tr><tr><td>128</td><td><span> </span><span style="color:#a7adba;">// Capture the text input value
</span></td></tr><tr><td>129</td><td><span> </span><span style="color:#b48ead;">let</span><span> user_prompt = </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">text_input</span><span>(id!(message_input)).</span><span style="color:#96b5b4;">text</span><span>();
</span></td></tr><tr><td>130</td><td><span>
</span></td></tr><tr><td>131</td><td><span> </span><span style="color:#a7adba;">// Set the text of the user message label
</span></td></tr><tr><td>132</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">label</span><span>(id!(user_message)).</span><span style="color:#96b5b4;">set_text</span><span>(&user_prompt);
</span></td></tr><tr><td>133</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">view</span><span>(id!(user_message_bubble)).</span><span style="color:#96b5b4;">set_visible</span><span>(</span><span style="color:#d08770;">true</span><span>);
</span></td></tr><tr><td>134</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">redraw</span><span>(cx);
</span></td></tr><tr><td>135</td><td><span>
</span></td></tr><tr><td>136</td><td><span> </span><span style="color:#a7adba;">// Replacing the hardcoded response with a real one
</span></td></tr><tr><td>137</td><td><span> </span><span style="color:#96b5b4;">send_message_to_chat_gpt</span><span>(cx, user_prompt);
</span></td></tr><tr><td>138</td><td><span> }
</span></td></tr><tr><td>139</td><td><span> }
</span></td></tr><tr><td>140</td><td><span>}
</span></td></tr><tr><td>141</td><td><span>
</span></td></tr><tr><td>142</td><td><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">send_message_to_chat_gpt</span><span>(</span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx, </span><span style="color:#bf616a;">message</span><span>: String) {
</span></td></tr><tr><td>143</td><td><span> </span><span style="color:#b48ead;">let</span><span> completion_url = "</span><span style="color:#a3be8c;">https://api.openai.com/v1/chat/completions</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>();
</span></td></tr><tr><td>144</td><td><span> </span><span style="color:#b48ead;">let</span><span> request_id = live_id!(SendChatMessage);
</span></td></tr><tr><td>145</td><td><span> </span><span style="color:#b48ead;">let mut</span><span> request = HttpRequest::new(completion_url, HttpMethod::</span><span style="color:#d08770;">POST</span><span>);
</span></td></tr><tr><td>146</td><td><span>
</span></td></tr><tr><td>147</td><td><span> request.</span><span style="color:#96b5b4;">set_header</span><span>(
</span></td></tr><tr><td>148</td><td><span> "</span><span style="color:#a3be8c;">Content-Type</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>(),
</span></td></tr><tr><td>149</td><td><span> "</span><span style="color:#a3be8c;">application/json</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>()
</span></td></tr><tr><td>150</td><td><span> );
</span></td></tr><tr><td>151</td><td><span>
</span></td></tr><tr><td>152</td><td><span> request.</span><span style="color:#96b5b4;">set_header</span><span>(
</span></td></tr><tr><td>153</td><td><span> "</span><span style="color:#a3be8c;">Authorization</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>(),
</span></td></tr><tr><td><mark style="background-color:#a7adba30;">154</mark></td><td><mark style="background-color:#a7adba30;"><span> "</span><span style="color:#a3be8c;">Bearer <YOUR_ACCESS_KEY></span><span>".</span><span style="color:#96b5b4;">to_string</span><span>()
</span></mark></td></tr><tr><td>155</td><td><span> );
</span></td></tr><tr><td>156</td><td><span>
</span></td></tr><tr><td>157</td><td><span> request.</span><span style="color:#96b5b4;">set_json_body</span><span>(ChatPrompt {
</span></td></tr><tr><td>158</td><td><span> messages: vec![Message {content: message, role: "</span><span style="color:#a3be8c;">user</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>()}],
</span></td></tr><tr><td>159</td><td><span> model: "</span><span style="color:#a3be8c;">gpt-3.5-turbo</span><span>".</span><span style="color:#96b5b4;">to_string</span><span>(),
</span></td></tr><tr><td>160</td><td><span> max_tokens: </span><span style="color:#d08770;">100
</span></td></tr><tr><td>161</td><td><span> });
</span></td></tr><tr><td>162</td><td><span>
</span></td></tr><tr><td>163</td><td><span> cx.</span><span style="color:#96b5b4;">http_request</span><span>(request_id, request);
</span></td></tr><tr><td>164</td><td><span>}
</span></td></tr><tr><td>165</td><td><span>
</span></td></tr><tr><td>166</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(SerJson, DeJson)]
</span></td></tr><tr><td>167</td><td><span style="color:#b48ead;">struct </span><span>ChatPrompt {
</span></td></tr><tr><td>168</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">messages</span><span>: Vec<Message>,
</span></td></tr><tr><td>169</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">model</span><span>: String,
</span></td></tr><tr><td>170</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">max_tokens</span><span>: </span><span style="color:#b48ead;">i32
</span></td></tr><tr><td>171</td><td><span>}
</span></td></tr><tr><td>172</td><td><span>
</span></td></tr><tr><td>173</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(SerJson, DeJson)]
</span></td></tr><tr><td>174</td><td><span style="color:#b48ead;">struct </span><span>Message {
</span></td></tr><tr><td>175</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">content</span><span>: String,
</span></td></tr><tr><td>176</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">role</span><span>: String
</span></td></tr><tr><td>177</td><td><span>}
</span></td></tr><tr><td>178</td><td><span>
</span></td></tr></tbody></table></code></pre>
<blockquote>
<p>Remember to use your own OpenAI access key in the line 154</p>
</blockquote>
<p>With the addition of <code>send_message_to_chat_gpt()</code> (lines 142-164), we can send a request to the ChatGPT API server. Note that we’re not yet handling the response so we can focus on the request part. The <code>cx.http_request</code> (line 163) is the mechanism in Makepad to issue regular HTTP requests in a non-blocking manner. This ensures that our application UI won’t be blocked while the response is still pending.</p>
<p>The <code>HttpRequest::set_json_body()</code> function (line 157) receives a struct representing the JSON format expected by the server. Note that we define the <code>ChatPrompt</code> struct (line 167) for this purpose, and then derive <code>SerJson</code> and <code>DeJson</code> on them to automatically generate efficient JSON parsing logic for them. You can think of those traits as a simpler version of <code>Serde</code>, which we are importing in the line 2.</p>
<p>Let’s now receive the responses and update the user interface:</p>
<pre data-linenos data-lang="rust" style="background-color:#eff1f5;color:#4f5b66;" class="language-rust "><code class="language-rust" data-lang="rust"><table><tbody><tr><td>114</td><td><span style="color:#b48ead;">impl </span><span>MatchEvent </span><span style="color:#b48ead;">for </span><span>App {
</span></td></tr><tr><td>129</td><td><span>
</span></td></tr><tr><td>130</td><td><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">handle_network_responses</span><span>(
</span></td></tr><tr><td>131</td><td><span> &</span><span style="color:#b48ead;">mut </span><span style="color:#bf616a;">self</span><span>,
</span></td></tr><tr><td>132</td><td><span> </span><span style="color:#bf616a;">cx</span><span>: &</span><span style="color:#b48ead;">mut</span><span> Cx,
</span></td></tr><tr><td>133</td><td><span> </span><span style="color:#bf616a;">responses</span><span>: &NetworkResponsesEvent
</span></td></tr><tr><td>134</td><td><span> ) {
</span></td></tr><tr><td>135</td><td><span> </span><span style="color:#b48ead;">let</span><span> label = </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">label</span><span>(id!(model_message));
</span></td></tr><tr><td>136</td><td><span> </span><span style="color:#b48ead;">for</span><span> event in responses {
</span></td></tr><tr><td>137</td><td><span> </span><span style="color:#b48ead;">match </span><span>&event.response {
</span></td></tr><tr><td>138</td><td><span> NetworkResponse::HttpResponse(response) => {
</span></td></tr><tr><td>139</td><td><span> </span><span style="color:#b48ead;">match</span><span> event.request_id {
</span></td></tr><tr><td>140</td><td><span> live_id!(SendChatMessage) => {
</span></td></tr><tr><td>141</td><td><span> </span><span style="color:#b48ead;">if</span><span> response.status_code == </span><span style="color:#d08770;">200 </span><span>{
</span></td></tr><tr><td>142</td><td><span> </span><span style="color:#b48ead;">let</span><span> chat_response =
</span></td></tr><tr><td>143</td><td><span> response.get_json_body::<ChatResponse>().</span><span style="color:#96b5b4;">unwrap</span><span>();
</span></td></tr><tr><td>144</td><td><span> label.</span><span style="color:#96b5b4;">set_text</span><span>(
</span></td></tr><tr><td>145</td><td><span> &chat_response.choices[</span><span style="color:#d08770;">0</span><span>].message.content
</span></td></tr><tr><td>146</td><td><span> );
</span></td></tr><tr><td>147</td><td><span> } </span><span style="color:#b48ead;">else </span><span>{
</span></td></tr><tr><td>148</td><td><span> label.</span><span style="color:#96b5b4;">set_text</span><span>(&format!(
</span></td></tr><tr><td>149</td><td><span> "</span><span style="color:#a3be8c;">Failed to connect with OpenAI: </span><span style="color:#d08770;">{:?}</span><span>",
</span></td></tr><tr><td>150</td><td><span> response.</span><span style="color:#96b5b4;">get_string_body</span><span>()
</span></td></tr><tr><td>151</td><td><span> ));
</span></td></tr><tr><td>152</td><td><span> }
</span></td></tr><tr><td>153</td><td><span>
</span></td></tr><tr><td>154</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">view</span><span>(id!(model_message_bubble)).</span><span style="color:#96b5b4;">set_visible</span><span>(</span><span style="color:#d08770;">true</span><span>);
</span></td></tr><tr><td>155</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">redraw</span><span>(cx);
</span></td></tr><tr><td>156</td><td><span> },
</span></td></tr><tr><td>157</td><td><span> _ => (),
</span></td></tr><tr><td>158</td><td><span> }
</span></td></tr><tr><td>159</td><td><span> }
</span></td></tr><tr><td>160</td><td><span> NetworkResponse::HttpRequestError(error) => {
</span></td></tr><tr><td>161</td><td><span> label.</span><span style="color:#96b5b4;">set_text</span><span>(
</span></td></tr><tr><td>162</td><td><span> &format!("</span><span style="color:#a3be8c;">Failed to connect with OpenAI </span><span style="color:#d08770;">{:?}</span><span>", error)
</span></td></tr><tr><td>163</td><td><span> );
</span></td></tr><tr><td>164</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">view</span><span>(id!(model_message_bubble)).</span><span style="color:#96b5b4;">set_visible</span><span>(</span><span style="color:#d08770;">true</span><span>);
</span></td></tr><tr><td>165</td><td><span> </span><span style="color:#bf616a;">self</span><span>.ui.</span><span style="color:#96b5b4;">redraw</span><span>(cx);
</span></td></tr><tr><td>166</td><td><span> }
</span></td></tr><tr><td>167</td><td><span> _ => ()
</span></td></tr><tr><td>168</td><td><span> }
</span></td></tr><tr><td>169</td><td><span> }
</span></td></tr><tr><td>170</td><td><span> }
</span></td></tr><tr><td>171</td><td><span>}
</span></td></tr><tr><td>209</td><td><span>
</span></td></tr><tr><td>210</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(SerJson, DeJson)]
</span></td></tr><tr><td>211</td><td><span style="color:#b48ead;">struct </span><span>ChatResponse {
</span></td></tr><tr><td>212</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">id</span><span>: String,
</span></td></tr><tr><td>213</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">object</span><span>: String,
</span></td></tr><tr><td>214</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">created</span><span>: </span><span style="color:#b48ead;">i32</span><span>,
</span></td></tr><tr><td>215</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">model</span><span>: String,
</span></td></tr><tr><td>216</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">usage</span><span>: Usage,
</span></td></tr><tr><td>217</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">choices</span><span>: Vec<Choice>,
</span></td></tr><tr><td>218</td><td><span> </span><span style="color:#b48ead;">pub </span><span style="color:#bf616a;">system_fingerprint</span><span>: Option<String>,
</span></td></tr><tr><td>219</td><td><span>}
</span></td></tr><tr><td>220</td><td><span>
</span></td></tr><tr><td>221</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(SerJson, DeJson)]
</span></td></tr><tr><td>222</td><td><span style="color:#b48ead;">pub struct </span><span>Usage {
</span></td></tr><tr><td>223</td><td><span> </span><span style="color:#bf616a;">prompt_tokens</span><span>: </span><span style="color:#b48ead;">i32</span><span>,
</span></td></tr><tr><td>224</td><td><span> </span><span style="color:#bf616a;">completion_tokens</span><span>: </span><span style="color:#b48ead;">i32</span><span>,
</span></td></tr><tr><td>225</td><td><span> </span><span style="color:#bf616a;">total_tokens</span><span>: </span><span style="color:#b48ead;">i32</span><span>,
</span></td></tr><tr><td>226</td><td><span>}
</span></td></tr><tr><td>227</td><td><span>
</span></td></tr><tr><td>228</td><td><span>#[</span><span style="color:#bf616a;">derive</span><span>(SerJson, DeJson)]
</span></td></tr><tr><td>229</td><td><span style="color:#b48ead;">struct </span><span>Choice {
</span></td></tr><tr><td>230</td><td><span> </span><span style="color:#bf616a;">message</span><span>: Message,
</span></td></tr><tr><td>231</td><td><span> </span><span style="color:#bf616a;">finish_reason</span><span>: String,
</span></td></tr><tr><td>232</td><td><span> </span><span style="color:#bf616a;">index</span><span>: </span><span style="color:#b48ead;">i32</span><span>,
</span></td></tr><tr><td>233</td><td><span> </span><span style="color:#bf616a;">logprobs</span><span>: Option<String>,
</span></td></tr><tr><td>234</td><td><span>}
</span></td></tr></tbody></table></code></pre>
<p>Makepad's <code>MatchEvent</code> trait has a <code>handle_network_responses()</code> function, and by implementing it (lines 130-171) we now have a way to network-related events. This function is quite straightforward once we define a <code>ChatResponse</code> struct (line 211) to represent the JSON response format coming from ChatGPT.</p>
<p>Once we retrieve the chat message from the response, we set the corresponding <code>Label</code> instance's text (line 144). We also make sure that the parent view is visible (line 154) and everything gets redrawn (line 155). The parent visibility was hidden because we only want to display the messages bubbles once we have the messages</p>
<p>If everything goes well with the ChatGPT API server, you should see an interaction like the following one:</p>
<p><img src="/blog/makepad-chatgpt-final-result.png" alt="ChatGPT response displayed" />
<em>This is what ChatGPT knows about Makepad...</em> 🙂</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>Let's take a look at what we have achieved in a few steps. We have a basic application with an easy-to-modify look and feel. In just a few dozen lines of code, we made a working chat client that shows real responses from ChatGPT models. Now, we invite you to try different platforms beyond the native desktop platform that we demonstrated in this post. Follow the instructions in the <a href="https://github.com/makepad/makepad/?tab=readme-ov-file#build--run-instructions">Makepad README</a> to see how to test this application in Android, iOS, and web.</p>
<p>In a future post, we'll cover how to implement a list of messages to make this app really feel like a true interactive chat app.</p>
<p><strong>Have fun hacking with Makepad and Rust!</strong></p>
Performance Benchmarking on iOS2024-05-25T00:00:00+00:002024-05-25T00:00:00+00:00
Unknown
https://robius.rs/blog/performance-benchmarking-ios/<p>After testing the <a href="/blog/performance-benchmarking">Performance Benchmarking</a> of cross-platform applications written with Makepad on Android platforms, we decided to do some tests on iOS.</p>
<p>Instead of using the exisitng mock WeChat and TaoBao apps, we had decided to focus more on graphics effects performance. For this, the team at WyeWorks had helped create a suite of graphics effects in an app called <strong>comp_demo</strong>.</p>
<div style="display:flex">
<div style="flex:1;padding-right:5px;">
<h2 id="graphics-effects">Graphics Effects</h2>
<p>The effects below have been implemented in all three apps:</p>
<ul>
<li>Rounded Corner</li>
<li>Bitmap Image RC</li>
<li>Bitmap Text Scaling</li>
<li>Vector Text Scaling</li>
<li>Transparency effects</li>
<li>Control Shadow</li>
<li>Path Shadow</li>
<li>Control Stroke</li>
</ul>
<h2 id="implementations">Implementations</h2>
<p>The same set of features were implemented using Swift UI to run as iOS native application, using Flutter to run on iOS.</p>
<p>Using Makepad, the same set of features were implemented and also ran on iOS.</p>
<h2 id="devices">Devices</h2>
<p>All 3 apps and tests were ran on the same iPhone 12 and iPhone 13 devices. The devices themselves were "clean" machines, meaning a new machine wihtout any extra applications installed, just the three test apps.</p>
<p>The phones were running iOS 17.3.</p>
</div>
<div style="flex:1;padding-left:5px;">
<a href="/blog/iphone-comp-demo-ss.jpg" target="_new">
<img src="/blog/iphone-comp-demo-ss.jpg" alt="iPhone screenshot" />
</a>
</div>
</div>
<!--  -->
<!-- <figure>
<img src="/blog/iphone-comp-demo-ss.jpg" width="400"
alt="Main Screen">
<figcaption>Makepad comp_demo screenshot.</figcaption>
</figure> -->
<h2 id="process">Process</h2>
<p>Collecting performance metrics on iOS is not as straightforward as on Android. Android's Perfetto tool automates most of the process of collecting and presenting the metrics, both in UI and JSON data formats. But on iOS, the Xcode's Instrumentation tool mainly deals with UI displays of real-time performance graphs. While this is helpful in some situations, it is not sufficient for the metrics that we wanted to collect.</p>
<p>Hence, we had to utilize the <code>xcrun xctrace</code> commands to record metrics we are interested in, then use export to extract the metrics from the proprietary binary file formats into an xml file, and then convert it into csv format in order to be more human readable using a spreadsheet application.</p>
<p>After running through the process several times, we devised scripts to automate as many of these manual steps as possible. This included running one command to start the recording process to do the test. And then one script to extract the proper data, and one more at the end to summarize the data that we're interested in, such as the CPU %, Memory usage, etc.</p>
<h2 id="results">Results</h2>
<p>The results were interesting to see. First, for most of the Swift UI version of the application, the data were not readily or easily seen. It seems that iOS "hides" or "combines" the actual application usage data with the operating system, such that it is not apparent which work was actually peformed and counted as part of the application and which were counted as OS.</p>
<p><a href="/blog/ios-results-table.png"><img src="/blog/ios-results-table.png" alt="Results Table" /></a>
(In the table above, red background means good, yellow means bad)</p>
<p>So the more comparable test results are between the Makepad and Flutter apps. As both of these applications come with their own rendering layer, and do not utilize iOS's own native rendering.</p>
<h3 id="rounded-corners-and-text-scaling">Rounded Corners and Text Scaling</h3>
<p>From these results, it can be seen that in general, Makepad app's CPU % usage is often ~50% less than what the Flutter app's were. On the other hand, Makepad app's GPU % usage is often ~30% higher.</p>
<p>Note that memory usage includes both the CPU and GPU memory in this case. Makepad's memory usage is in general much higher than the others. Part of the reason is that currently Makepad still bundles its own big font file as part of the app, which results in about 40MB of memory usage on its own.</p>
<p>As part of the Project Robius's platform abstraction efforts, we hope to add feature for Makepad (and other Rust UI applications) to be able to easily use the underlying platform's system fonts. This will help reduce the memory footprint by a large amount.</p>
<p>These results are actually very similar to those seen in the previous <a href="/blog/performance-benchmarking-2">Performance Benchmarking</a> of scrolling tests on Android. At that time, Makepad apps also exhibited the lower CPU but higher GPU usage pattern.</p>
<p>This can be attributed to Makepad's architecture design of doing some work on CPU but doing a lot of other graphics manipulation on the GPU directly by utilizing shader programs. This allows certain graphics operations to be as simple as changing a variable, without the need for repeated drawcalls. Makepad also optimizes drawcalls by minimizing the number of them. It also has the flexibility of allowing users to write shader programs to execute on the GPU.</p>
<h3 id="shadow-effects">Shadow Effects</h3>
<p>For the shadow effects, we see the huge advantage that Makepad app has over the equivalent Flutter and even Swift apps. Makepad app is the only one that managed a steady 60 fps rendering, while the Swift and Flutter apps struggled to stay in the ~45 fps range.</p>
<p>The large CPU % spike also indicates that it is working extra hard to perform these graphics shadow effects.</p>
<h2 id="summary">Summary</h2>
<p>For a relatively new (~4 years) UI framework written in Rust, Makepad's performance can be seen as quite impressive, especially consdering it has been developed mainly by a team of 3 people.</p>
<p>Part of the performance advantages can be attributed to the efficiency of the programming language Rust itself. But a major reason can be attributed to Makepad's novel design approach of having a mixed immediate mode + retained mode rendering system. Currently, the framework has not even begun to optimize specifically for performance tests. For example, the higher memory footprint is a current design decision, and has room for furter optimization according to <a href="https://x.com/rikarends">Rik</a>.</p>
<p>We're looking forward to continuing to work closely with the Makepad team to bring the best cross-platform performance UI framework in Rust to all developers.</p>
<h2 id="references">References</h2>
<p>All tests were performed on the iOS build of the Makepad comp_demo app, running on iPhone 12 and iPhone 13.</p>
<p>Makepad comp_demo app:
<a href="https://github.com/project-robius/comp_demo"><code>https://github.com/project-robius/comp_demo</code></a></p>
<p>iOS Benchmarking scripts:
<a href="https://github.com/project-robius/benchmarking-ios"><code>https://github.com/project-robius/benchmarking-ios</code></a></p>
<p>Makepad Framework:
<a href="https://github.com/makepad/makepad"><code>https://github.com/makepad/makepad</code></a></p>
Performance Improvements with SDF2024-01-29T00:00:00+00:002024-01-29T00:00:00+00:00
Unknown
https://robius.rs/blog/performance-improvements-with-sdf/<p>Recently, the <a href="https://github.com/makepad/makepad">Makepad</a> framework updated its font handling with SDF generation algorithms. We decided to test out the WeChat and TaoBao apps built using this latest iteration of the framework to see if there are any changes to performance. The tests are same as before: fast scrolling of the main content screen back and forth within a 10 second window.</p>
<p>The WeChat and TaoBao apps' code have pretty much stayed the same. The only change was made last month to work with Makepad's then-new <code>MatchEvent()</code> event handling paradigm, which simplified the code base and reduced the size of it by 20~30%.</p>
<p>The SDF algorithm changes in Makepad Framework itself is transparent to the application developer. It is a Makepad internal performance optimization.</p>
<h2 id="results-summary">Results Summary</h2>
<h3 id="makepad-taobao">Makepad TaoBao</h3>
<p><img src="/blog/makepad-sdf-taobao-results.png" alt="TaoBao Results Summary" /></p>
<p>As can be seen from the preliminary results, the CPU Cycles of the application reduced by about 12%, while the average and max CPU frequency went up by 18% and 56% respectively. This is because the SDF generation takes more CPU processing initially to process the fonts, but once it's done, the overall CPU usage is reduced during subsequent rendering.</p>
<p>The CPU memory usage increased by an insignificant amount. But the GPU memory usage both reduced by over 23%. This is a significant improvement over the old Makepad way of using the Font Atlas feature. The dynamic SDF generation algorithm uses less than 1/4 of the GPU memory, a huge improvement.</p>
<h3 id="makepad-wechat">Makepad WeChat</h3>
<p><img src="/blog/makepad-sdf-wechat-results.png" alt="WeChat Results Summary" /></p>
<p>Unlike the TaoBao app, the WeChat app is more "text-centric", with less images and mostly text-based rendering. The improvements here are more apparent.</p>
<p>The CPU cycles reduced by 24%, while the CPU frequency increases by average of 15% and max of 21% respectively.</p>
<p>The GPU memory usage reductions were even more significant, with 35% less average memory usage and 32% decrease in max memory usage.</p>
<p>The detailed chart of each of the metrics follows below.</p>
<h2 id="individual-graphs">Individual Graphs</h2>
<h3 id="cpu-cycles">CPU Cycles</h3>
<p>On the CPU cycle, we see the significant reduction of CPU cycles with the new SDF algorithm as compared to the old Font Atlas rendering.</p>
<p>For the mostly text-based app such as WeChat, the results are more significant.</p>
<blockquote>
<p><em>(For all graphs, the right side bar(s) is the new Makepad with SDF.)</em></p>
</blockquote>
<table><thead><tr><th>Makepad TaoBao</th><th>Makepad WeChat</th></tr></thead><tbody>
</tbody></table>
<div style="display:flex">
<div style="flex:1;padding-right:5px;">
<a href="/blog/cpu-cycles-3.png" target="_new">
<img src="/blog/cpu-cycles-3.png" width="100%" alt="CPU Cycles" />
</a>
</div>
<div style="flex:1;padding-left:5px;">
<a href="/blog/cpu-cycles-4.png" target="_new">
<img src="/blog/cpu-cycles-4.png" width="100%" alt="CPU Cycles" />
</a>
</div>
</div>
<h3 id="cpu-frequency">CPU Frequency</h3>
<p>The only metric where the new method resulted in higher values due to running of the SDF algorithm.</p>
<table><thead><tr><th>Makepad TaoBao</th><th>Makepad WeChat</th></tr></thead><tbody>
</tbody></table>
<div style="display:flex">
<div style="flex:1;padding-right:5px;">
<a href="/blog/cpu-frequency-3.png" target="_new">
<img src="/blog/cpu-frequency-3.png" width="100%" alt="CPU Frequency" />
</a>
</div>
<div style="flex:1;padding-left:5px;">
<a href="/blog/cpu-frequency-4.png" target="_new">
<img src="/blog/cpu-frequency-4.png" width="100%" alt="CPU Frequency" />
</a>
</div>
</div>
<h3 id="cpu-memory-usage">CPU Memory Usage</h3>
<p>The CPU memory usage had minimal increases compared to the previous method.</p>
<table><thead><tr><th>Makepad TaoBao</th><th>Makepad WeChat</th></tr></thead><tbody>
</tbody></table>
<div style="display:flex">
<div style="flex:1;padding-right:5px;">
<a href="/blog/cpu-memory-3.png" target="_new">
<img src="/blog/cpu-memory-3.png" width="100%" alt="CPU Memory" />
</a>
</div>
<div style="flex:1;padding-left:5px;">
<a href="/blog/cpu-memory-4.png" target="_new">
<img src="/blog/cpu-memory-4.png" width="100%" alt="CPU Memory" />
</a>
</div>
</div>
<h3 id="gpu-memory-usage">GPU Memory Usage</h3>
<p>The GPU memory shows reflects the biggest difference between the old and new algorithms. Both averagea and maximum GPU memory usage were reduced by over 30%.</p>
<table><thead><tr><th>Makepad TaoBao</th><th>Makepad WeChat</th></tr></thead><tbody>
</tbody></table>
<div style="display:flex">
<div style="flex:1;padding-right:5px;">
<a href="/blog/gpu-memory-3.png" target="_new">
<img src="/blog/gpu-memory-3.png" width="100%" alt="GPU Memory" />
</a>
</div>
<div style="flex:1;padding-left:5px;">
<a href="/blog/gpu-memory-4.png" target="_new">
<img src="/blog/gpu-memory-4.png" width="100%" alt="GPU Memory" />
</a>
</div>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>The results of these tests validates the benefits of the SDF generation algorithm that the latest Makepad has incorporated. CPU Cycles were reduced, indicating less processing after the initial ramp up. With SDF, Makepad can also cache the generation of the SDF on local storage to lower the CPU usage.</p>
<p>The GPU memory use reduction is the most significant difference. The usage of the SDF generation algorithm reduced GPU memory usage by a large amount. In addition, it will also provide improved performance for dynamically scaled texts.</p>
<h2 id="references">References</h2>
<p>All tests were performed on the Android build of the Makepad TaoBao and Makepad WeChat apps, running on Google Pixel 7 Pro. The tests were run a minimum of 6 times each, and the results were averaged.</p>
<p>Makepad WeChat sample app:
<a href="https://github.com/project-robius/makepad_wechat"><code>https://github.com/project-robius/makepad_wechat</code></a></p>
<p>Makepad TaoBao sample app:
<a href="https://github.com/project-robius/makepad_taobao"><code>https://github.com/project-robius/makepad_taobao</code></a></p>
<p>Android Benchmarking scripts:
<a href="https://github.com/project-robius/benchmarking-android"><code>https://github.com/project-robius/benchmarking-android</code></a></p>
<p>Makepad Framework:
<a href="https://github.com/makepad/makepad"><code>https://github.com/makepad/makepad</code></a></p>
<p>SDF Generation Algorithm:
<a href="https://github.com/LykenSol/sdfer"><code>https://github.com/LykenSol/sdfer</code></a></p>
Performance Benchmarking - Part 22023-11-17T00:00:00+00:002023-11-17T00:00:00+00:00
Unknown
https://robius.rs/blog/performance-benchmarking-2/<p>Several days ago we did a <a href="/blog/performance-benchmarking">performance benchmark</a> test between sample apps written in Android "native" vs. Makepad. Afterwards, we were given access to another version of the sample TaoBao app with more functionality, including more animation/video content in the scroll list, and the ability to turn toggle some of the special effects.</p>
<p>So we did the same scrolling tests with this new version, both with the special features turned on, as well as with all features turned OFF, in order to compare the differences.</p>
<h2 id="updated-results">Updated Results</h2>
<p><img src="/blog/scrolling-test-table-2.png" alt="" /></p>
<p>As can be seen from the results, they seem similar and consistent with our previous results from the image manipulation benchmarks.</p>
<h3 id="cpu-processing">CPU Processing</h3>
<p><img src="/blog/cpu-cycles-2.png" alt="" /></p>
<p>On the CPU cycle, the sample Makepad WeChat app shows very little cycles used, having less than half of the Android "native" sample apps.</p>
<p>The Native TaoBao apps were similar to the Official TaoBao in that they use a lot more CPU cycles. This also results in higher CPU frequency values.</p>
<p>When special functionality are turned OFF, the Native TaoBao used ~40% less cycles. While the full-effect TaoBao used slightly more than the Official TaoBao app.</p>
<p><img src="/blog/cpu-frequency-2.png" alt="" /></p>
<p>The CPU frequencies also reflect that Makepad uses much less than the Android native counterparts.</p>
<p>Not much difference were noticed with the new Native TaoBao versions for GPU memory usage.</p>
<h3 id="memory-usage">Memory Usage</h3>
<p>The CPU memory usage is more similar among the Android native and Makepad sample applications, with Makepad apps using slightly less CPU memory. This is consistent with less usage of the CPU cycles as well.</p>
<p>As expected, the new Native TaoBao performed noticeably better when its special effects were turned off.</p>
<p><img src="/blog/cpu-memory-2.png" alt="" /></p>
<p>The GPU memory shows that Makepad apps use more memory than the Android native apps. This is currently the only area where Makepad is not as efficient. However, the Makepad team is currently working on an improvement that will decrease this usage in the near future.</p>
<p>Not much difference were noticed with the new Native TaoBao versions for GPU memory usage.</p>
<p><img src="/blog/gpu-memory-2.png" alt="" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>Being a more fully developed Android Native TaoBao app, we noticed much smoother and faster scrolling speed and effects compared to the previous simpler "native" apps. The performance characteristics of this version matches more closely to the official Play Store TaoBao, though with less CPU memory footprint.</p>
<p>Overall, the results still reflect what we had found in our previous benchmarking tests. The Makepad versions of the apps have consistently outperformed all other versions, at least in terms of pure scrolling tests.</p>
<h2 id="references">References</h2>
<p>Makepad WeChat sample:
<a href="https://github.com/project-robius/makepad_wechat"><code>https://github.com/project-robius/makepad_wechat</code></a></p>
<p>Makepad TaoBao sample:
<a href="https://github.com/project-robius/makepad_taobao"><code>https://github.com/project-robius/makepad_taobao</code></a></p>
Performance Benchmarking2023-11-13T00:00:00+00:002023-11-13T00:00:00+00:00
Unknown
https://robius.rs/blog/performance-benchmarking/<p>The last time we did a performance benchmark test, it was a few months ago. At that time we used only one simple application, an image manipulation program. During the GOSIM Workshop in September, I had mentioned that we would be doing more performance benchmarking tests and we have done just that.</p>
<p>This time we used Makepad to write a sample WeChat application and a sample TaoBao application. The main test criteria is to stress the "scrolling" feature of the apps. We also had access to two sample applications of the same written in Android "native".</p>
<h2 id="test-methodology">Test Methodology</h2>
<p>Scrolling is one of the most important and common operations for mobile applications. For WeChat, a messaging application, and TaoBao, an e-commerce application, a smooth scrolling experience is often one of the key factors in user satisfaction with the application.</p>
<p>We benchmarked similar performance metrics as last time using Google <a href="https://ui.perfetto.dev">Perfetto</a> tool. These include:</p>
<ul>
<li>CPU Cycles</li>
<li>CPU Frequency (Average & Max)</li>
<li>CPU Memory (Average & Max)</li>
<li>GPU Memory (Average & Max)</li>
</ul>
<p>To test, we use the applications' main message or product list page. We exercise the scrolling by quickly swiping up and down on this scrollable page. We first quickly swipe for 6 to 10 times up (to make the list go down) and then 6 to 10 times down and repeat, until we reach the 10 seconds sample time.</p>
<p><em>(Note that in real applications the scrolling list might have network dependencies such as loading of images, etc. But for the sample applications, the images are cached locally to the mobile app so there’s no network latency or variance.)</em></p>
<p>In order to have some more data points, we also benchmarked the official TaoBao app from the Google Play Store.</p>
<h2 id="results">Results</h2>
<p><img src="/blog/scrolling-test-table.png" alt="" /></p>
<p>As can be seen from the results, they seem similar and consistent with our previous results from the image manipulation benchmarks.</p>
<h3 id="cpu-processing">CPU Processing</h3>
<p><img src="/blog/cpu-cycles.png" alt="" /></p>
<p>On the CPU cycle, the sample Makepad WeChat app shows very little cycles used, having less than half of the Android native sample app.</p>
<p><img src="/blog/cpu-frequency.png" alt="" /></p>
<p>The CPU frequencies also reflect that Makepad uses much less than the Android native counterparts.</p>
<h3 id="memory-usage">Memory Usage</h3>
<p>The CPU memory usage is more similar among the Android native and Makepad sample applications, with Makepad apps using slightly less CPU memory. This is consistent with less usage of the CPU cycles as well.</p>
<p><img src="/blog/cpu-memory.png" alt="" /></p>
<p>The GPU memory shows that Makepad apps use more memory than the Android native apps. This is currently the only area where Makepad is not as efficient. However, the Makepad team is currently working on an improvement that will decrease this usage in the near future.</p>
<p><img src="/blog/gpu-memory.png" alt="" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>Being a fully functioning application, the official TaoBao app had a much higher CPU cycle and CPU memory than the rest.</p>
<p>Also, there seems to be multiple processes related to the TaoBao application. We only counted the main TaoBao process. If we were to add the data from the TaoBao GPU process, then it will increase the amount of GPU memory usage by about 70 MB.</p>
<p>Overall, the results are consistent to what we found in our early benchmarking test. As Makepad framework continues to improve and evolve, the numbers will undoubtably change. We will periodically run more of these performance benchmarking and post our updates.</p>
<h2 id="references">References</h2>
<p>Makepad WeChat sample:
<a href="https://github.com/project-robius/makepad_wechat"><code>https://github.com/project-robius/makepad_wechat</code></a></p>
<p>Makepad TaoBao sample:
<a href="https://github.com/project-robius/makepad_taobao"><code>https://github.com/project-robius/makepad_taobao</code></a></p>