見出し画像

Learning Iced | Part 1

In this post, I’d like to break down the components of the Iced to-do app to ensure that I fully understand how the app is structured.

Iced is a beautifully designed GUI API written in Rust. If you want the speed of native applications—especially if you’re a Rust enthusiast—this is definitely a go-to crate for building your native applications!

Without further ado, let’s dive in and learn something new. Just a disclaimer: this post might be divided into several parts, as it could require a lot of writing to cover everything in one post, so please bear that in mind.

The code we’re going to examine is from the “todos” example. If you’re curious or want to follow along, please clone the project, and don’t forget to give it a star. :)


Skimming

First things first, let’s go over the code from top to bottom.

main

pub fn main() -> iced::Result {
    #[cfg(not(target_arch = "wasm32"))]
    tracing_subscriber::fmt::init();

    iced::application(Todos::title, Todos::update, Todos::view)
        .subscription(Todos::subscription)
        .font(include_bytes!("../fonts/icons.ttf").as_slice())
        .window_size((500.0, 800.0))
        .run_with(Todos::new)
}

Okay, so iced::application looks like this: it takes title, update, and view.

iced::application

pub fn application<State, Message, Theme, Renderer>(
    title: impl Title<State>,
    update: impl Update<State, Message>,
    view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>,
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
where
    State: 'static,
    Message: Send + std::fmt::Debug + 'static,
    Theme: Default + DefaultStyle,
    Renderer: program::Renderer,
{
    ...
}

title is the name of the app, so it’s just a string.
update handles the update logic.
view likely refers to the components or elements that make up the app’s UI.

We’ll examine Todos::update and Todos::view later, but for now, let’s continue with the rest of the components. I’ll skip .font() and .window_size() since they seem self-explanatory.

Alright, what’s subscription?

In computer programming, a subscription generally refers to a mechanism where a piece of code (often called a “subscriber”) expresses interest in receiving updates or notifications from another piece of code (often called a “publisher” or “observable”). This pattern is widely used in various contexts, such as event-driven programming, reactive programming, and message-passing systems.

ChatGPT 4o

Thanks to my previous job, I’m actually familiar with this term. I would use it in situations where one variable depends on another variable that’s likely to change.

For example, imagine you are at a crosswalk. You observe the traffic light. If it turns green, you start walking; otherwise, you remain still.

In this scenario, you are “subscribed” to the traffic light because your reaction changes according to its state.

Todos::subscription

impl Todos {
    ...

    fn subscription(&self) -> Subscription<Message> {
        use keyboard::key;

        keyboard::on_key_press(|key, modifiers| {
            let keyboard::Key::Named(key) = key else {
                return None;
            };

            match (key, modifiers) {
                (key::Named::Tab, _) => Some(Message::TabPressed {
                    shift: modifiers.shift(),
                }),
                (key::Named::ArrowUp, keyboard::Modifiers::SHIFT) => {
                    Some(Message::ToggleFullscreen(window::Mode::Fullscreen))
                }
                (key::Named::ArrowDown, keyboard::Modifiers::SHIFT) => {
                    Some(Message::ToggleFullscreen(window::Mode::Windowed))
                }
                _ => None,
            }
        })
    }
    
    ...
}

Looking at what Todos::subscription does, it appears the method performs the following:

  • The method subscription is setting up a subscription to keyboard events in an application.

  • The closure provided to keyboard::on_key_press maps specific key presses (Tab, ArrowUp + Shift, ArrowDown + Shift) to corresponding messages (Message::TabPressed, Message::ToggleFullscreen).

  • If the key press matches one of the specified patterns, the appropriate message is returned. Otherwise, no action is taken (None is returned).

  • The application can then handle these messages, likely using some form of message-passing or event-handling mechanism to respond to user inputs like switching to fullscreen or detecting when the Tab key is pressed.

Next, let’s look at run_with(). The documentation says, ‘Runs the Application with a closure that creates the initial state.’ So, let’s take a look at the Todos::new method.

Todos::new

impl Todos {
    fn new() -> (Self, Command<Message>) {
        (
            Self::Loading,
            Command::perform(SavedState::load(), Message::Loaded),
        )
    }
    
    ...
}

Got it. I guess there’s not much happening here; it just seems to load the saved state. And that’s about it… lol.

Okay, let's move on and look at Todos::view next.

Todos::view

impl Todos {
    ...
    
    fn view(&self) -> Element<Message> {
        match self {
            Todos::Loading => loading_message(),
            Todos::Loaded(State {
                input_value,
                filter,
                tasks,
                ..
            }) => {
                let title = text("todos")
                    .width(Fill)
                    .size(100)
                    .color([0.5, 0.5, 0.5])
                    .align_x(Center);

                let input = text_input("What needs to be done?", input_value)
                    .id(INPUT_ID.clone())
                    .on_input(Message::InputChanged)
                    .on_submit(Message::CreateTask)
                    .padding(15)
                    .size(30);

                let controls = view_controls(tasks, *filter);
                let filtered_tasks =
                    tasks.iter().filter(|task| filter.matches(task));

                let tasks: Element<_> = if filtered_tasks.count() > 0 {
                    keyed_column(
                        tasks
                            .iter()
                            .enumerate()
                            .filter(|(_, task)| filter.matches(task))
                            .map(|(i, task)| {
                                (
                                    task.id,
                                    task.view(i).map(move |message| {
                                        Message::TaskMessage(i, message)
                                    }),
                                )
                            }),
                    )
                    .spacing(10)
                    .into()
                } else {
                    empty_message(match filter {
                        Filter::All => "You have not created a task yet...",
                        Filter::Active => "All your tasks are done! :D",
                        Filter::Completed => {
                            "You have not completed a task yet..."
                        }
                    })
                };

                let content = column![title, input, controls, tasks]
                    .spacing(20)
                    .max_width(800);

                scrollable(container(content).center_x(Fill).padding(40)).into()
            }
        }
    }
    
    ...
}

Whoa, this is a bit lengthy, but that’s okay.

This is essentially where the entire user interface resides. Let’s run the app and see what it looks like.”

Nice!

Okay, I need to eat again… brb


いいなと思ったら応援しよう!