Introduction

libcosmic is the platform toolkit for COSMIC—a GUI toolkit for creating COSMIC-themed applets and applications. Based on the cross-platform iced GUI library—which it utilizes for its runtime and rendering primitives—the COSMIC toolkit features personalizable desktop theming, a responsive widget library, a configuration system, platform integrations, and its own interface guidelines for building consistent and responsive applications.

As a Rust-based GUI toolkit, experience with Rust is required. Rust's rich type system and language features are key to what makes the COSMIC toolkit a much friendlier developer experience—enabling secure, reliable, and efficient applications to be developed at a faster pace than would be possible otherwise. For those interested in learning Rust, there are a lot of good resources available: Learn Rust in a Month of Lunches, Rust in Action, Rust by Example, the official Rust Book, and Rustlings.

Although the toolkit was created for the COSMIC desktop environment, it is also cross-platform, and thus it can be used to build COSMIC-themed applications for Linux (X11 & Wayland), Redox OS, Windows, and Mac. Even mobile platforms could be a possibility someday. One of the goals of libcosmic is to enable the creation of a cross-platform ecosystem of applications that are easy to port from one OS to another. We would also welcome any that would like to build their own OS experiences with the COSMIC toolkit.

Model-View-Update (MVU)

The Iced runtime is an implementation of the MVU (Model-View-Update) design pattern—also known as TEA (The Elm Architecture). The MVU design pattern is a functional approach to GUI design that consists of an event loop with ownership of the application's struct—aka the Model, a view function for generating a View from that model, and an update function for updating the Model.

Similar to how Elm was created, this architecture also emerged naturally in the Rust ecosystem as everyone searched for ways to model applications and services which adhere to Rust's aliasing XOR mutability rule. This can be seen with the rise of similar frameworks, such as Sauron, Relm4, and tui-realm. At any given point, the application's model is either being immutably borrowed by its view, or is being mutably borrowed by its update method. Thus it eliminates the need for shared references, interior mutability, and runtime borrow checking.

To describe this in code, see the iced.rs book example here:

use magic::{display, interact};

// Initialize the state
let mut counter = Counter::default();

// Be interactive. All the time! 
loop {
    // Run our view logic to obtain our interface
    let interface = counter.view();

    // Display the interface to the user
    display(&interface);

    // Process the user interactions and obtain our messages
    let messages = interact(&interface);

    // Update our state by processing each message
    for message in messages {
        counter.update(message);
    }
}

In each iteration of the event loop, the runtime calls the view method of the application's Model to create a new View. The View is a state machine whose purpose is both to describe the layout of the interface and how to draw it; and to be a streamlined pipeline for processing UI events and yielding any Messages from widgets in the View that they triggered. The View can efficiently borrow data directly from the Model because the View has the same lifetime as the borrowed Model.

Because the majority of the runtime is likely to be spent in drawing, the runtime will diff the layout and state of the View to detect when there is a need to redraw a node in the widget tree. The runtime will also cache certain elements between frames—such as images—to prevent the need to redraw them.

Once the View has been drawn, the runtime will wait for UI events—such as mouse and keyboard events—and process them directly through the View. Those events will be pass through various widgets which may emit any number of Messages in response. After the View has processed the UI event(s), the View is dropped and any received Messages will be passed through the Model's update method, which mutably borrows the Model.

The update method uses pattern matching to find the appropriate branch to execute (which is much faster than dynamic dispatch), and the programmer can then update the model while running any application logic necessary. Once the update method has completed, the next iteration of the loop begins.

The Application Trait

Before beginning, I would recommend cloning the COSMIC App Template to experiment with while reading the documentation below.

Model

Every application begins with the application model. All application state will be stored in this model, and it will be wise to cache data that will be needed by your application's widgets. It is important that it contains at least a cosmic::app::Core, which will provide a way of interacting with certain aspects of the COSMIC app runtime.

use cosmic::prelude::*;

struct AppModel {
    core: cosmic::app::Core,
    counter: u32,
    counter_text: String,
}

Message

Alongside that struct, there will also be a Message type, which describes the kinds of events that widgets in the applications are going to emit.

#[derive(Debug, Clone)]
pub enum Message {
    Clicked
}

The Trait

Together, these will be used to create a cosmic::Application. Implementing this trait will automatically generate all of the necessary code to run a COSMIC application which integrates consistently within the COSMIC desktop.

Note that the following associated types and constants are required:

  • Executor is the async executor that will be used to run your application's commands.
  • Flags is the data that your application needs to use before it starts.
  • Message is the enum that contains all the possible variants that your application will need to transmit messages.
  • APP_ID is the unique identifier of your application.

We also need to provide methods to enable the COSMIC app runtime to access the application's Core.

impl cosmic::Application for AppModel {
    type Executor = cosmic::executor::Default;
    type Flags = ();
    type Message = Message;

    const APP_ID: &str = "tld.domain.AppName";

    fn core(&self) -> &Core {
        &self.core
    }

    fn core_mut(&mut self) -> &mut Core {
        &mut self.core
    }
}

Init

This is where your application model will be constructed, and any necessary tasks scheduled for execution on init. This will typically be where you want to set the name of the window title.

fn init(core: Core, _flags: Self::Flags) -> (Self, Command<Self::Message>) {
    let mut app = AppModel {
        core,
        counter: 0,
        counter_text: String::new(),
    };

    app.counter_text = format!("Clicked {} times", app.counter);
  
    let command = app.set_window_title("AppName");

    (app, command)
}

View

At the beginning of each iteration of the runtime's event loop, the view method will be called to create a view which describes the current state of the UI. The returned state machine defines the layout of the interface, how it is to be drawn, and what messages widgets will emit when triggered by certain UI events.

impl cosmic::Application for AppModel {
    ...
    
    /// The returned Element has the same lifetime as the model being borrowed.
    fn view(&self) -> Element<Self::Message> {
        let button = widget::button(&self.counter_text)
            .on_press(Message::Clicked);
            
        widget::container(button)
            .width(iced::Length::Fill)
            .height(iced::Length::Shrink)
            .center_x()
            .center_y()
            .into()
    }
}

This method will be composed from widget functions that you can get from the cosmic::widget module. Note that widgets are composed functionally, and therefore they are designed to have their fields set through a Builder pattern.

Update

Messages emitted by the view will later be passed through the application's update method. This will use Rust's pattern matching to choose a branch to execute, make any changes necessary to the application's model, and may optionally return one or more commands.

impl cosmic::Application for AppModel {
    ...
    
    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        match message {
            Message::Clicked => {
                self.counter += 1;
                self.counter_text = format!("Clicked {} times", self.counter);
            }
        }
        
        Command::none()
    }
}

Because this method executes in the runtime's event loop, the application will block for the duration that this method is being called. It is therefore imperative that any application logic executed here should be swift to prevent the user from experiencing an application freeze. Anything that requires either asynchronous or long execution time should either be returned as a Command, or placed into a Subscription.

Running the application

Once the trait has been implemented, you can run it from your main function like so:

fn main() -> cosmic::iced::Result {
    let settings = cosmic::app::Settings::default();
    cosmic::app::run::<AppModel>(settings, ())
}

Commands

Commands are short-lived async tasks that are spawned onto an async executor on a background thread. They must return a message back to the application upon completion, and cannot directly send messages back to the application until they return.

NOTE: While it is not possible for a command to directly send messages before completion, it is possible to create a subscription from a channel which passes its sender to the application, which may then pass that sender into its commands.

Future

Commands may be created from futures using cosmic::command::future.

fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
    match message {
        Message::Clicked => {
            self.counter += 1;
            self.counter_text = format!("Clicked {} times", self.counter);
            
            // Await for 3 seconds in the background, and then request to decrease the counter.
            return cosmic::command::future(async move {
                tokio::time::sleep(Duration::from_millis(3000)).await;
                Message::Decrease
            });
        }
        
        Message::Decrease =>  {
            self.counter -= 1;
            self.counter_text = format!("Clicked {} times", self.counter);
        }
    }
    
    Command::none()
}

Batches

They can also be batched for concurrent execution, where messages will be received in the order of completion.

fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
    match message {
        Message::BatchStarted => {
            eprintln!("started handling batch");
        }

        Message::Clicked => {
            self.counter += 1;
            self.counter_text = format!("Clicked {} times", self.counter);
            
            // Run two async tasks concurrently.
            return cosmic::command::batch(vec![
                // Await for 3 seconds in the background, and then request to decrease the counter.
                cosmic::command::future(async move {
                    tokio::time::sleep(Duration::from_millis(3000)).await;
                    Message::Decrease
                }),
                // Immediately returns a message without waiting.
                cosmic::command::message(Message::BatchStarted)
            ]);
        }
        
        Message::Decrease =>  {
            self.counter -= 1;
            self.counter_text = format!("Clicked {} times", self.counter);
        }
    }
    
    Command::none()
}

Widget Operations

They can also be used to perform an operation onto a widget, such as focusing a button or text input.

return cosmic::widget::button::focus(self.BUTTON_ID);

Subscriptions

Subscriptions are long-running async tasks which listen for external events passively, and forward Messages back to the application runtime. They can be used to continuously monitor events for the entire lifetime of the application.

Channels

The most common form of a subscription will be that of a channel. This will effectively behave as an async generator which yields messages to the application runtime. The source of your events could be from a channel, async stream, or a custom event loop.

struct MySubscription;
let subscription = cosmic::subscription::channel(
    std::any::TypeId::of::<MySubscription>(),
    4,
    move |mut output| async move {
        let stream = streamable_operation();

        while let Some(event) = stream.next().await {
            let _res = output.send(Message::StreamedMessage(event)).await;
        }

        futures::future::pending().await
    },
);

Batches

If your application needs more than one Subscription, you can batch them together in one with Subscription::batch.

Subscription::batch(vec![
    subscription1,
    subscription2,
    subscription3,
])

Forwarding messages from commands

This trick enables Commands to yield Messages to the application before they are finished.

struct MessageForwarder;
let subscription = cosmic::subscription::channel(
    std::any::TypeId::of::<MessageForwarder>(),
    4,
    move |mut output| async move {
        let (tx, mut rx) = tokio::sync::mpsc::channel::<Message>(4);

        let _res = output.send(Message::RegisterSubscriptionSender(tx)).await;

        while let Some(event) = rx.recv().await {
            let _res = output.send(event).await;
        }

        futures::future::pending().await
    },
);

A channel will be created which sends its Sender directly to the application with a Message. You will store this message inside of your application like so:

Message::RegisterSubscriptionSender(sender) => {
    self.sender = Some(sender);
}

Then you can clone the sender when creating commands that need to forward messages back to the runtime.

Nav Bar

COSMIC's Nav Bar is a common element found in most applications with navigatable interfaces. The cosmic::Application trait comes with some predefined methods that can be optionally set to enable integration with the Nav Bar with minimal setup.

First, it is necessary to add the nav_bar::Model to your application's model.

struct AppModel {
    /// A model that contains all of the pages assigned to the nav bar panel.
    nav: nav_bar::Model,
}

The nav bar can then be enabled by implementing these methods in your cosmic::Application trait.

/// Enable the nav bar to appear in your application when `Some`.
fn nav_model(&self) -> Option<&nav_bar::Model> {
    Some(&self.nav)
}

/// Activate the nav item when selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
    // Activate the page in the model.
    self.nav.activate(id);
}

Items can be added and modified from the init or update methods.

fn init(core: Core, _flags: Self::Flags) -> (Self, Command<Self::Message>) {
    let mut nav = nav_bar::Model::default();

    nav.insert()
        .text("Page 1")
        .data::<Page>(Page::Page1)
        .icon(icon::from_name("applications-science-symbolic"))
        .activate();

    nav.insert()
        .text("Page 2")
        .data::<Page>(Page::Page2)
        .icon(icon::from_name("applications-system-symbolic"));

    nav.insert()
        .text("Page 3")
        .data::<Page>(Page::Page3)
        .icon(icon::from_name("applications-games-symbolic"));

    let mut app = YourApp {
        core,
        nav,
    };

    (app, Command::none())
}

Each item in the model can hold any number of custom data types, which can be fetched by their type.

if let Some(page) = self.nav.data::<Page>().copied() {
    eprintln!("the current page is {page}");
}

MenuBar

It is also recommended for applications to provide menu bars whenever they have sufficient need to display a variety of selectable options. See the cosmic::widget::menus module for more details on the APIs available for menu creation.

In the future, menu bars will be a source for interacting with global menus.

Defining MenuAction(s)

Menu bars have their own custom message types. This one will provide just an about settings page.

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MenuAction {
    About,
}

For this type to be usable with a menu bar, it needs to implement the menu::Action trait. This defines which application message that the menu action should convert into.

impl menu::Action for MenuAction {
    type Message = Message;

    fn message(&self) -> Self::Message {
        match self {
            MenuAction::About => Message::ToggleContextPage(ContextPage::About),
        }
    }
}

Keybindings

Your preferred key bindings for these menu actions should also be attached to your application's model.

struct AppModel {
    /// Key bindings for the application's menu bar.
    key_binds: HashMap<menu::KeyBind, MenuAction>,
}

Add to cosmic::Application

You can add then add a menu bar to the start of your application's header bar by defining this method in your cosmic::Application implementation.

/// Elements to pack at the start of the header bar.
fn header_start(&self) -> Vec<Element<Self::Message>> {
    let menu_bar = menu::bar(vec![menu::Tree::with_children(
        menu::root(fl!("view")),
        menu::items(
            &self.key_binds,
            vec![menu::Item::Button(fl!("about"), MenuAction::About)],
        ),
    )]);

    vec![menu_bar.into()]
}

Context Drawer

COSMIC applications use the Context Drawer to display additional application context for a select context. This overlay widget will be placed above the contents of the window on the right side of the application window.

Context Page

As the context drawer is a reusable element, you want to define a type for describing which context to show. To start with, we will make a context page which shows an about page. This will require that your application has the menu bar added to it from the previous chapter.

/// Identifies a context page to display in the context drawer.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ContextPage {
    #[default]
    About,
}

impl ContextPage {
    fn title(&self) -> String {
        match self {
            Self::About => fl!("about"),
        }
    }
}

You will also want to assign this to your application model

struct AppModel {
    /// Display a context drawer with the designated page if defined.
    context_page: ContextPage,
}

cosmic::Application integration

The context_drawer method can be defined to show the context drawer. When this method returns an Element, the context drawer will be displayed. The COSMIC runtime keeps track of when the context drawer should be shown, so we can use this as a hint to when we can show it or not. How you define the view of this page is up to you.

/// Display a context drawer if the context page is requested.
fn context_drawer(&self) -> Option<Element<Self::Message>> {
    if !self.core.window.show_context {
        return None;
    }

    Some(match self.context_page {
        ContextPage::About => self.about(),
    })
}

Toggling the context drawer

In the previous chapter, we defined a message for toggling the context drawer and assigning the page. This glue will toggle the visibility of the context drawer, assign the context page, and set the title of the context drawer. Note that the set_context_title is a method from cosmic::ApplicationExt. This method sets the title of the context page in the cosmic::app::Core.

match message {
    Message::ToggleContextPage(context_page) => {
        if self.context_page == context_page {
            // Close the context drawer if the toggled context page is the same.
            self.core.window.show_context = !self.core.window.show_context;
        } else {
            // Open the context drawer to display the requested context page.
            self.context_page = context_page;
            self.core.window.show_context = true;
        }

        // Set the title of the context drawer.
        self.set_context_title(context_page.title());
    }
}

Dialogs

Structure

Given that all application state will be stored into one top level application struct, you may be wondering how to manage the complexity of the model. The general advice is not to feel bad about having a model with a lot of fields, a large view method with a many lines of code, and a large update method with many message variants. This is a feature of the MVU design pattern which centralizes the logic in a way that is easy to refactor and reorganize as your application grows naturally.

Use descriptive names when adding new fields to your application model, and group fields together into structs when you feel that it is necessary.

Likewise, you may find it useful to create additional message types which you can wrap in your top level message enum.

enum Message {
    VariantA,
    VariantB,
    VariantC(CMessage)
}

Widgets

Text

The text module provides a variety of standard typography presets to use in your applications.

let body = widget::text::body("Body");
let caption = widget::text::caption("Caption");
let caption_heading = widget::text::caption_heading("Caption Heading");
let heading = widget::text::heading("Heading");
let monotext = widget::text::monotext("Monotext");
let title1 = widget::text::title1("Title 1");
let title2 = widget::text::title2("Title 2");
let title3 = widget::text::title3("Title 3");

Container

Column

Row

Buttons

Icon

Image

Svg

Divider

Space

Text Input

Toggler

Slider

Radio

Check Box

Dropdown

Spin Button

Color Picker

Flex Row

Grid

Segmented Buttons

Tab Bar

Segmented Controls

Context Menu

Pane Grid

Creating a Widget

Creating an Overlay

Examples

Todo