├── .gitignore
├── LICENSE-MIT
├── README.md
└── posts
└── 001
├── 0.0.1.md
├── hello-world.png
└── pressed-18.png
/.gitignore:
--------------------------------------------------------------------------------
1 | /.DS_Store
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Frui: Blog
2 |
3 | Simple repository for blog posts from Frui.
4 |
5 | Main repo: https://github.com/fruiframework/frui
--------------------------------------------------------------------------------
/posts/001/0.0.1.md:
--------------------------------------------------------------------------------
1 | #
blog
2 |
3 | ##### A developer-friendly framework for building user interfaces in Rust.
4 |
5 |
6 | # Frui 0.0.1
7 |
8 | *October 14, 2022, posted by [iglak](https://github.com/toiglak)*
9 |
10 | Hi, I'm iglak. In this post, I want to introduce you to the very first version of [Frui](https://github.com/fruiframework/frui) - a developer-friendly framework for building user interfaces in Rust. Here, I will explain its basic architecture and talk about what motivated Frui and its design choices in the first place. Finally, I'll show you the current state of the features and what's next.
11 |
12 |
13 |
14 |
15 |
16 | Crab counter made in Frui
17 | (see the code)
18 |
19 |
20 | *Author's note: this is my first blog post ever and in my second language at that, so please tell me if something could be improved :) Frui is a framework inspired by Flutter. Because of that, those who are familiar with Flutter should be able to grasp concepts in this post intuitively.*
21 |
22 | ## Frui app
23 |
24 | Let's start with the simplest application you can run.
25 |
26 | ```rust
27 | use frui::prelude::*;
28 |
29 | fn main() {
30 | run_app(());
31 | }
32 | ```
33 |
34 | With that our first Frui application is ready! It obviously does nothing (apart from opening an empty window), but let's explain what happens. run_app is a function that starts up the framework, and as an argument it takes a widget to display. In this case that widget is unit type `()` which renders nothing.
35 | You can compose widgets together to build more complicated UIs:
36 |
37 | ```rust
38 | fn main() {
39 | run_app(Center::child(Text::new("Hello, World!")));
40 | }
41 | ```
42 |
43 | Here we wrap Text widget in Center widget. After we run this is what we will see:
44 |
45 |
46 |
47 |
48 |
49 |
50 | Composing widgets in this way makes it possible to write complicated UIs while keeping them simple and easy to understand.
51 |
52 | ### View widget
53 |
54 | For an application to be scalable there must be a way to abstract parts of the UI interface. In Frui this can be done using `ViewWidget`:
55 |
56 | ```rust
57 | #[derive(ViewWidget)]
58 | struct HelloWorld;
59 |
60 | impl ViewWidget for HelloWorld {
61 | fn build<'w>(&'w self, _: BuildContext<'w, Self>) -> Self::Widget<'w> {
62 | Center::child(Text::new("Hello, World!"))
63 | }
64 | }
65 |
66 | fn main() {
67 | run_app(HelloWorld);
68 | }
69 | ```
70 |
71 | In the above code, the previous UI fragment is put inside of the `HelloWorld`’s `ViewWidget` implementation. That way you can now use that same piece of interface in multiple places by referencing the `HelloWorld` widget.
72 |
73 | Do note that the above code uses some of the **Rust nightly** features to work, so for the above to compile you will need to add the following two lines to your crate root.
74 |
75 | ```rust
76 | #![feature(min_specialization)]
77 | #![feature(type_alias_impl_trait)]
78 | ```
79 |
80 | Last important thing I want to introduce is `WidgetState`:
81 |
82 | ```rust
83 | impl WidgetState for HelloWorld {
84 | type State = usize;
85 |
86 | fn create_state(&self) -> Self::State { 0 }
87 | }
88 | ```
89 |
90 | By implementing above, the `HelloWorld` widget can now hold state. With that we can make use of the `BuildContext` argument passed to the `build` method:
91 |
92 | ```rust
93 | impl ViewWidget for HelloWorld {
94 | fn build<'w>(&'w self, ctx: BuildContext<'w, Self>) -> Self::Widget<'w> {
95 | KeyboardEventDetector {
96 | on_event: |_| *ctx.state_mut() += 1,
97 | child: Center::child(
98 | Text::new(format!("Pressed {} times.", *ctx.state()))
99 | ),
100 | }
101 | }
102 | }
103 | ```
104 |
105 | `KeyboardEventDetector` is a widget, which calls `on_event` callback every time a key is pressed. Additionally in the child field of that widget we placed our previous hello world code.
106 |
107 | Inside of that callback we increment the state of our widget (accessed through `state_mut`) and in response to that Frui rebuilds `HelloWorld` widget, thus updating the UI.
108 | With that we get the following:
109 |
110 |
111 |
112 |
113 |
114 |
115 | Full code:
116 |
117 | ```rust
118 | #![feature(type_alias_impl_trait)]
119 | #![feature(min_specialization)]
120 |
121 | use frui::prelude::*;
122 |
123 | #[derive(ViewWidget)]
124 | struct HelloWorld;
125 |
126 | impl WidgetState for HelloWorld {
127 | type State = usize;
128 |
129 | fn create_state(&self) -> Self::State { 0 }
130 | }
131 |
132 | impl ViewWidget for HelloWorld {
133 | fn build<'w>(&'w self, ctx: BuildContext<'w, Self>) -> Self::Widget<'w> {
134 | KeyboardEventDetector {
135 | on_event: |_| *ctx.state_mut() += 1,
136 | child: Center::child(
137 | Text::new(format!("Pressed {} times.", *ctx.state()))),
138 | }
139 | }
140 | }
141 |
142 | fn main() {
143 | run_app(HelloWorld);
144 | }
145 | ```
146 |
147 | As a side note, `WidgetState` has more methods that can be implemented, e.g. `mount` and `unmount`. This is directly inspired by Flutter.
148 |
149 | ## Frui goals
150 |
151 | Since the beginning, the main goal of Frui was to make a simple and extensible API. One that is both easy to implement and refactor. Frui reasons that a productive developer means better software. And that is exactly the goal: **to enable developers to write UI code efficiently and avoid typical pain points of other libraries**.
152 |
153 | This goal already dictated some of the design decisions, like:
154 |
155 | - allow data to be passed from parent to child widget through simple references (widgets can be non-static) and allow widgets to be generic (this allows children widgets to be inlined)
156 | - avoid macros where possible, so that tools like rust-analyzer can work well
157 | - automatically infer return type of the build method, to avoid manually type erasing return value or manually writing [TAIT](https://github.com/rust-lang/rust/issues/63063) inside of the associated type
158 | - use builder pattern to avoid `..Default::default()` when using the core widgets
159 | - iterate on the API before releasing and study what worked and what didn’t in other ui frameworks
160 | - use Rust with its awesome features (algebraic types, pattern matching, traits, crates, etc.)
161 |
162 | One consequence of that goal is that Frui doesn’t aim to be the most efficient UI library in Rust. Of course it doesn’t mean that Frui doesn’t care about performance at all, but compared to other libraries Frui aims to be more high-level, where the main focus is put on simplicity, intuitiveness, and maintainability.
163 |
164 | ## Interesting bits
165 |
166 | ### Generic widgets
167 |
168 | Even though the preferred way of constructing widgets in Frui is using the builder pattern, which one could assume means boxing and erasing the type of those widgets in builder methods, widgets in Frui can be fully generic and the state of such widgets is preserved between rebuilds.
169 |
170 | Thanks to that, some builder methods in Frui tend to look like this:
171 |
172 | ```rust
173 | impl Container {
174 | pub fn child(self, child: C) -> Container { ... }
175 | }
176 | ```
177 |
178 |
179 |
180 | Example use of such API
181 |
182 | In the following example you can similar builder method in action:
183 |
184 | 1. `Column::builder()` returns `Column<()>` type.
185 | 2. Column is then configured with different methods.
186 | 3. Finally, `children` builder method updates the children list in the column, turning the type of the column into `Column<(Square, Square, Square)>`.
187 |
188 | Throughout all of this, the column widget remains generic and no children are boxed.
189 |
190 | ```rust
191 | #[derive(ViewWidget)]
192 | struct App;
193 |
194 | impl ViewWidget for App {
195 | fn build<'w>(&'w self, _: BuildContext<'w, Self>) -> Self::Widget<'w> {
196 | Column::builder()
197 | .space_between(10.0)
198 | .main_axis_size(MainAxisSize::Max)
199 | .cross_axis_size(CrossAxisSize::Max)
200 | .main_axis_alignment(MainAxisAlignment::SpaceEvenly)
201 | .cross_axis_alignment(CrossAxisAlignment::Center)
202 | .children((
203 | Square(Color::rgb8(13, 245, 152)),
204 | Square(Color::rgb8(255, 0, 110)),
205 | Square(Color::rgb8(0, 186, 255)),
206 | ))
207 | }
208 | }
209 | ```
210 |
211 |
212 |
213 |
214 |
215 | Why it matters
216 |
217 | It’s about preserving the state of widgets.
218 |
219 | Let’s show this in an example. Say there's a `Parent` widget and a generic `Child` widget that has state `S`. When `Parent` widget builds `Child`, the state `S` of that widget is initialized:
220 |
221 | ```
222 | Parent —> Child (S)
223 | ```
224 |
225 | Let's say that after a `Parent` widget rebuilds, it will return the same `Child` widget but with a different generic type (this often happens when the child of a generic widget is changed).
226 |
227 | ```
228 | * Parent rebuilds *
229 | Parent —> Child<()> (S)
230 | ```
231 |
232 | In the above simple scenario, even though `TypeId`s of `Child` and `Child<()>` are different, the state `S` of the `Child` widget is preserved.
233 |
234 | You can see this in action in the [`preserve_state.rs` example](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/preserve_state.rs).
235 |
236 |
237 |
238 | ### Non-static widgets
239 |
240 | Frui makes it possible to have non-static widgets. This eliminates the need for reference counting and makes it easy to pass data from `self` to child widget!
241 |
242 | ```rust
243 | #[derive(ViewWidget)]
244 | struct App<'a> {
245 | text: &'a str,
246 | }
247 |
248 | impl ViewWidget for App<'_> {
249 | fn build<'w>(&'w self, _: BuildContext<'w, Self>) -> Self::Widget<'w> {
250 | Center::child(Text::new(&self.text))
251 | }
252 | }
253 | ```
254 |
255 | This is often used to pass a reference to a child stored in a struct field:
256 |
257 | ```rust
258 | #[derive(ViewWidget)]
259 | struct App {
260 | child: W, // W doesn't require 'static bound!
261 | }
262 |
263 | impl ViewWidget for App {
264 | fn build<'w>(&'w self, _: BuildContext<'w, Self>) -> Self::Widget<'w> {
265 | Center::child(&self.child)
266 | }
267 | }
268 |
269 | run_app(App {
270 | child: Text::new("Hello!"),
271 | });
272 | ```
273 |
274 | ### More interesting bits
275 | There are a few more things in Frui that you may find interesting! Check out the following examples, which come with explanations:
276 |
277 | - [local key example](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/local_key.rs) - preserving state of children widgets in multi-child widgets (related to “stable widget identity”),
278 | - [inherited widget example](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/inherited_widget.rs) - how to define and use an inherited widget which allows for (1) accessing common state from any place in that widget’s subtree and (2) rebuilding only those widgets which depend on it,
279 | - [column](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/column.rs) and [row](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/row.rs) [examples](https://github.com/fruiframework/frui/tree/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples) - how different options of those widgets affect the way their children are laid out,
280 | - [crab counter example!](https://github.com/fruiframework/frui/blob/fe96a1752e314c8c7a4f287ce98a2a7f81c298fd/examples/crab_counter.rs) - get a taste of how *fun* building “real applications'' in Frui could be
281 |
282 | ## Future
283 |
284 | ### Backend
285 |
286 | The back-end of the Frui needs a lot of work. It was basically made as a proof of concept, so that I could focus on building the API.
287 |
288 | There is still a lot to do, for example implementing event handling correctly so it is aware of z-index (widgets in front of other widgets) and making it in such a way that won't make it hard to implement focus and accessibility features later on. Another important thing that needs to be implemented in the back-end are various optimizations of layout and paint algorithms. The API for event handling, layout, and painting can still be experimented with and I hope to find something that will work very well and will scale up to large applications.
289 |
290 | ### More widgets
291 |
292 | Frui has only a handful of widgets right now. Apart from implementing more of them, the goal is to have two kinds of widgets:
293 |
294 | - a set of core widgets that are common across different design languages (like columns, rows, and containers),
295 | - and a second set of widgets specifically for building apps according to a particular design language (like Material Design).
296 |
297 | Many of these widgets can be adapted from Flutter, which has a similar structure to Frui. That way we can also figure out what works and doesn't work, so we end up with an even better API in the end.
298 |
299 | ### Async
300 |
301 | Rust is a great fit for asynchronous programming because of its features and ecosystem. I think that async should be a core part of any UI framework and because of that, they should be integrated to work well together.
302 |
303 | In the future, I want writing asynchronous code in Frui to be as simple as:
304 |
305 | ```rust
306 | #[derive(ViewWidget)]
307 | struct PageContent<'a> {
308 | link: &'a str,
309 | }
310 |
311 | impl ViewWidget for PageContent<'_> {
312 | fn build<'w>(&'w self, ctx: BuildContext<'w, Self>) -> Self::Widget<'w> {
313 | let res = ctx.spawn_future(async {
314 | let body = reqwest::get(self.link).await?;
315 | body.text().await
316 | });
317 |
318 | match res {
319 | Poll::Pending => LoadingCircle().boxed(),
320 | Poll::Ready(Ok(s)) => SuccessWidget(s).boxed(),
321 | Poll::Ready(Err(e)) => ErrorWidget(e).boxed(),
322 | }
323 | }
324 | }
325 | ```
326 |
327 | ### Better resource bundling
328 |
329 | I was thinking about how to utilize Rust's module system and macros to improve the way assets are typically handled. Existing solutions feel wonky or prone to errors, so I wanted to see if there was a better way.
330 |
331 | The idea is to create a macro that would work similarly to `include!`. It would issue a compile time error immediately when the path to an asset is incorrect. When compiling for release, it would put and link those resources in a separate asset bundle to not bloat the executable.
332 |
333 | Module system would then be used to organize those imports in the code.
334 |
335 | Say we have following structure of the assets directory:
336 |
337 | ```rust
338 | // /assets
339 | // \_ /brand
340 | // \_ brand-logo.png
341 | // \_ mod.rs
342 | // \_ mod.rs
343 | ```
344 |
345 | In `assets/brand/mod.rs` we would define:
346 |
347 | ```rust
348 | const BRAND_LOGO: Asset = include_asset!("brand-logo.png");
349 | ```
350 |
351 | And then to import it we would simply write:
352 |
353 | ```rust
354 | #[derive(ViewWidget)]
355 | struct App;
356 |
357 | impl ViewWidget for App {
358 | fn build<'w>(&'w self, ctx: BuildContext<'w, Self>) -> Self::Widget<'w> {
359 | Image::new(assets::brand::BRAND_LOGO)
360 | }
361 | }
362 | ```
363 |
364 | Again, this is all just me daydreaming, but I think doing it that way provides a lot of benefits over the existing solutions. I imagine that refactoring code like this would be much easier and it would be much harder to mess things up.
365 |
366 | ## Finishing thoughts
367 |
368 | I began working on Frui after having experienced the fun of putting together different lego blocks of an UI in Flutter and wanted to capture that same feeling in Rust. I think Frui succeeded in capturing that joy!
369 |
370 | Still, if Frui is to become something more than just a prototype a lot of work is needed. Currently the biggest limiting factor for Frui’s growth is my little understanding of things related to backend (especially the things I [mentioned before](#future)). I decided to release Frui in its current state with hope that I will be able to reach people that can help bringing Frui’s vision to life. If you feel like you know one thing or two about UI frameworks [please reach out](https://github.com/fruiframework/frui)!
371 |
372 | I hope you could find something interesting in this blog post. Frui is an ambitious project but I am sure it will bring some interesting ideas to Rust. Thank you for reading and I hope to see you soon on [Frui's github page](https://github.com/fruiframework/frui)!
373 |
--------------------------------------------------------------------------------
/posts/001/hello-world.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fruiframework/frui-blog/a4bb832d225e0f7d672fa83888520bfc9665daba/posts/001/hello-world.png
--------------------------------------------------------------------------------
/posts/001/pressed-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fruiframework/frui-blog/a4bb832d225e0f7d672fa83888520bfc9665daba/posts/001/pressed-18.png
--------------------------------------------------------------------------------