Introduction

Before beginning, I would recommend using the official COSMIC App Template to build applications with while reading the documentation below. You can generate a cosmic application using the cargo-generate utility with cargo generate gh:pop-os/cosmic-app-template.

libcosmic is 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 an advanced and responsive widget library based on COSMIC's design language, which supports personalizable desktop themes, cross-desktop theming integrations, a consistent interface guidelines, a standardized configuration system, and platform integrations.

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 any Linux distribution (X11 & Wayland), Redox OS, Windows, Mac, and even mobile platforms like Android. The goal of the cosmic library is to enable the creation of a cross-platform ecosystem of desktop 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.

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.

Model-View-Update (MVU)

Iced is a GUI libray for Rust which uses the MVU (Model-View-Update) architecture—also known as TEA (The Elm Architecture). The MVU architecture consists of a single event loop with exclusive ownership of the application model, a view function for creating views from the model, and an update function for updating the model. The model creates the view, the view is displayed to the user, the user sends inputs to the view, and any messages emitted by widgets in view are used to update the model.

A simplified abstract code example is provided below.

use magic::{display, interact}; // Initialize the state let mut app = AppModel::init(); // Be interactive. All the time! loop { // Run our view logic to obtain our interface let view = view(&app); // Display the interface to the user display(&view); // Process the user interactions and obtain our messages let messages = interact(&view); // Update our state by processing each message for message in messages { update(&mut app); } }

View logic

In each iteration of the event loop, the runtime begins by calling the view function with a reference to the application's model. The application author will use this function to construct the entire layout of their interface. Combining widget elements together until they are one—the View.

The View is a widget element itself that contains a tree of widget elements inside of it. Each with their own set of functions for performing layout, drawing, and event handling. Together, the View serves its role as a state machine that the runtime will use to render the application and the intercept application inputs.

Widgets in the View are stateless. They rely directly on the model as the single source of truth for their state. With the combination of Rust and the way the Iced library was architected, they can even borrow their values directly from the application model. Therefore, the View is a direct reflection of the current state of the model at any given point in time.

As Views are replaced in each iteration, the runtime will use an optimization technique to compare the differences with the previous View in order to decide which widgets in the layout need to be redrawn, and if any cached widget data should be culled.

If you were to create a widget that contains an image, the runtime will retain any image buffers it generates from the source image for reuse as long as the image widget remains in the tree. Similarly, pre-rendered text buffers will also be cached for reuse.

Update logic

After the View has been drawn, the runtime will wait for UI events to intercept—such as mouse and keyboard events—and pass them through the View's widget tree. Widgets that receive these events through their own internal update methods can decide to emit Message(s) to the runtime in response. The application author defines which messages will be emitted when those conditions are met.

Once messages have been emitted, they are passed directly to the update function for the application author to handle. In addition to updating the state of the model, the application may also decide to spawn tasks for execution in the background. These will execute asynchronously on background thread(s), and may emit messages back to the runtime over the course of their execution.

Similar to how Elm was created, this architecture has emerged naturally across the Rust ecosystem as a viable and efficient method of modeling 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.

The Application Trait

Model

use cosmic::prelude::*; struct App { counter: u32, counter_text: String, }

Every application begins with a model. This will be used as the single source of truth for your entire application and its GUI. This will include widget labels, text inputs, fetched icons, and any other values that need to be passed or referenced as input parameters when creating elements. Widgets are stateless, which means that they do not contain application state within themselves, but instead rely on the application model as the source for their state.

use cosmic::prelude::*; struct App { core: cosmic::Core, // ... }

The cosmic library also has some state of its own that you will need to store in your application model, refferred to as the cosmic::Core. This is managed internally by the cosmic runtime, but can also be used by the application to get and set certain runtime parameters.

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 App { 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, cosmic::app::Task<Self::Message>) { let mut app = App { 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 App { ... /// 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 App { ... fn update(&mut self, message: Self::Message) -> cosmic::app::Task<Self::Message> { match message { Message::Clicked => { self.counter += 1; self.counter_text = format!("Clicked {} times", self.counter); } } Task::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 Task, 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::<App>(settings, ()) }

Tasks

Since the update function is called from the same event loop that renders the application and processes user inputs, the GUI will block for the duration that the application spends inside of the update function. To avoid blocking the GUI, any operation(s) other than what is necessary to update the model should placed into tasks.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<Self::Message>> { cosmic::task::future(async move { Message::BuildResult(build().await) }) }

Tasks enable applications to execute operations asynchronously on background thread(s) without blocking the GUI. Returned as the output of the update function, they are spawned for concurrent execution on an async executor running on a background thread. Tasks based on futures return their output as a message to the application upon completion. Whereas tasks based on streams can stream messages to the application throughout their execution.

Avoid blocking the async executor

However, for the same reason that the GUI blocks when an update function is executing, similar is true for the thread where the async executor is scheduling the execution of its futures. The default executor for COSMIC applications is a tokio runtime configured to use a single background thread for scheduling async tasks. So if the application needs to spawn many futures on the runtime to execute concurrently, any operation that would block the executor should be moved onto another thread with tokio::task::spawn_blocking.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<Self::Message>> { match message { Message::WorkUnitReceived(work_unit) => { cosmic::task::future(async move { Message::WorkUnitResult(tokio::spawn_blocking(move || { fold_protein("0x23", 110, 80, 19, work_unit) }).await) }) } // ... } }

COSMIC Actions

The cosmic runtime has its own message type for handling updates to the cosmic runtime: cosmic::app::Action. To enable the cosmic runtime to handle messages simultaneously for itself and the application, the application's Message type is wrapped alongside cosmic::app::Action in the cosmic::Action<Message> type.

Since there are situations where applications may need to send messages to the cosmic runtime, all Application methods which return Tasks are defined to return cosmic::Task<cosmic::Action<Message>>. This means that you may see a type error if you try to return a cosmic::Task directly with your application's Message type without mapping it cosmic::Action::App beforehand. The cosmic::task module contains functions which automatically convert application messages into cosmic::Action<Message>.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<Self::Message>> { // Create a task that emits an application message without needing to await the value. let app_task = cosmic::Task::done(Message::ApplicationEvent) .map(cosmic::Action::from); // Create a cosmic action directly let show_window_menu = cosmic::Task::done(cosmic::app::Action::ShowWindowMenu) .map(cosmic::Action::from); // Use a helper from the ApplicationExt trait to create a cosmic task let set_window_title = self.set_window_title("Custom application title".into()); cosmic::Task::batch(vec![app_task, show_window_menu, set_window_title]) }

Futures

Tasks may be created from futures using cosmic::task::future.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<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::task::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() }

Streaming

Alternatively, they can produced from types which implement Stream. Such as from the receiving end of a channel which it is being pushed to from anothre thread.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<Self::Message>> { match message { Message::Start => { self.progress = Some(0); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(3)); _ = tx.send(Message::Progress(25)); std::thread::sleep(std::time::Duration::from_secs(3)); _ = tx.send(Message::Progress(50)); std::thread::sleep(std::time::Duration::from_secs(3)); _ = tx.send(Message::Progress(75)); std::thread::sleep(std::time::Duration::from_secs(3)); _ = tx.send(Message::Progress(100)); }); return cosmic::Task::stream(tokio_stream::wrappers::UnboundedReceiverStream(rx)) // Must wrap our app type in `cosmic::Action`. .map(cosmic::Action::App); } Message::Progress(progress) => { self.progress = Some(progress); } } cosmic::Task::none() }

Channel

Streams can be created directly from a future with an async channel using cosmic::iced_futures::stream::channel. This is commonly used as an alternative to the lack of async generators in Rust.

fn update(&mut self, message: Self::Message) -> cosmic::Task<cosmic::Action<Self::Message>> { match message { Message::Start => { self.progress = Some(0); return cosmic::Task::stream(cosmic::iced_futures::stream::channel(|tx| async move { tokio::time::sleep(std::time::Duration::from_secs(3)).await; _ = tx.send(Message::Progress(25)).await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; _ = tx.send(Message::Progress(50)).await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; _ = tx.send(Message::Progress(75)).await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; _ = tx.send(Message::Progress(100)).await; })) // Must wrap our app type in `cosmic::Action`. .map(cosmic::Action::App); } Message::Progress(progress) => { self.progress = Some(progress); } } cosmic::Task::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) -> cosmic::Task<cosmic::Action<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::task::batch(vec![ // Await for 3 seconds in the background, and then request to decrease the counter. cosmic::task::future(async move { tokio::time::sleep(Duration::from_millis(3000)).await; Message::Decrease }), // Immediately returns a message without waiting. cosmic::task::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);

Chaining

If you need to configure multiple tasks for execution, where some tasks depend on the completion of another before they start, the Task::chain method can be used to allow the execution of one task to begin only after the first has finished.

cosmic::task::future(async move { build().await }) .chain(cosmic::task::future(async move { clean().await }))

Aborting

This gives an abort handle to the application that you can store in your application to cancel a running task.

let (task, abort_handle) = cosmic::task::future(async move { tokio::time::sleep(std::time::Duration::from_secs(3)); println!("task finished"); Message::Finished })); abort_handle.abort();

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, ])

Structure

As the complexity of the application increases, so too does the needs of the application's model, messages, and logic. To prevent the application from becoming untenable, it will be necessary to periodically organize the structure of the application. There are two primary methods of reducing the complexity of your application model and logic: modules and states.

Modules

Modules are used to encapsulate related private data into a central type; along with its own message type and functions. These could be individual pages of your application, sections of a page, or even a reusable widgets composed of smaller widgets.

Below is a hypothetical application which contains two modules: todo and config. Each containing their own respective Page and Message types.

struct App { active_page: PageId, todo_page: todo::Page, config_page: config::Page, }

Starting with todo page module, which manages todo tasks.

mod todo { use cosmic::prelude::*; use cosmic::widget; pub async fn load() -> Message { // .. } #[derive(Debug, Clone)] pub enum Message { /// Add a new task Add, /// Edit an existing task EditInput(usize, String), /// Move the given task down MoveDown(usize), /// Move the given task up MoveUp(usize), /// Update the new task input editor NewInput(String), /// Remove an existing task Remove(usize) /// Save to disk Save } pub struct Page { new_task_input: String, tasks: Vec<String>, } impl Page { pub fn view(&self) -> cosmic::Element<Message> { // Where new tasks will be input before being added to the task list. let new_task_input = widget::text_input("Write down a new task here", &self.new_task_input) .on_input(Message::NewInput) .on_submit(Message::Add); // Fold each enumerated task into a widget that is pushed to a scrollable column. let saved_tasks = self.tasks.iter() .enumerate() .fold(widget::column(), |column, (id, task)| { column.push( // A hypothetical widget created for this app crate::widget::task(task.as_str()) .on_remove(Message::Remove(id)) .on_input(|text| Message::EditInput(id, text)) .on_move_down(Message::MoveDown(id)) .on_move_up(Message::MoveUp(id)) .into() ) }) .apply(widget::scrollable); // Compose the above widgets into the column view. widget::column::with_capacity(2) .spacing(cosmic::theme::active().cosmic().spacing.space_l) .push(new_task_input) .push(saved_tasks) .into() } pub fn update(&mut self, message: Message) -> cosmic::Task<cosmic::Action<Message>> { match message { Message::Add => { self.tasks.insert(std::mem::take(&mut self.new_task_input)); } Message::EditInput(id, task) => { self.tasks[id] = task; } Message::MoveDown(id) => { if id + 1 < self.tasks.len() { self.tasks.swap(id, id + 1); } } Message::MoveUp(id) => { if id > 0 { self.tasks.swap(id, id - 1); } } Message::NewInput(input) => { self.new_task_input = input; } Message::Remove(id) => { self.tasks.remove(id); } Message::Save => { // Hypothetical method to save the tasks to disk. let save_future = self.save_to_disk(); return cosmic::task::future(save_future); } } cosmic::Task::none() } } }

And now the config module:

mod config { #[derive(Debug, Clone)] pub enum Message { OpenUrl(url::Url) } pub struct Page { author_name: String, donate_url: url::Url, homepage_url: url::Url, repository_urlL: url::Url, } impl Page { pub fn view(&self) -> cosmic::Element<Message> { // Hypothetical config page } pub fn update(&mut self, message: Message) -> cosmic::Task<cosmic::Action<Message>> { match message { OpenUrl(url) => { tokio::spawn(open_url(url)); } } cosmic::Task::none() } } pub async fn open_url(url: url::Url) { // ... } }

We can then use them in your application's own native view and update functions like so:

use std:: #[derive(Debug, Clone)] enum Message { SetPage(PageId), ConfigPage(config::Message), TodoPage(todo::Message), } impl From<config::Message> for Message { fn from(message: config::Message) -> Self { Self::ConfigPage(config::Message) } } impl From<todo::Message> for Message { fn from(message: todo::Message) -> Self { Self::TodoPage(todo::Message) } } #[derive(Debug, Clone)] enum PageId { Config Todo, } // ... fn view(&self) -> cosmic::Element<Message> { match self.active_page { PageId::Todo => self.todo_page.view().map(Message::TodoPage), PageId::Config => self.config_page.view().map(Message::ConfigPage), } } fn update(&mut self, message: Message) -> cosmic::Task<cosmic::Action<Message>> { match message { Message::SetPage(id) => { self.active_page = id; match self.active_page { PageId::Config => (), PageId::Todo => return cosmic::task::future(async move { Message::TodoPage(todo::load().await) }), } } Message::ConfigPage(message) => { self.config_page.update(message).map(Into::into) } Message::TodoPage(message) => { self.todo_page.update(message).map(Into::into) } } }

We may even implement the Application::on_close_requested() method in our app to handle that Save message for our todo page.

fn on_close_requested(&mut self) -> Option<Message> { Some(Message::TodoPage(todo::Message::Save)) }

States

One of the most useful aspects of Rust for application development is the ability to use sum types with pattern matching to implement state machines. Messages received by the application can apply transitions to states within the application seamlessly. Which can make logic errors less likely to occur when you remove the need to guess the state based on values alone.

enum Package { Downloading { package: String, progress: usize, total: usize, time: std::time::Duration }, Installed { package: String }, Installable { package: String }, } struct Installer { package: Option<Package>, }

At any given moment, the state of the package in the Installer has four possible variants: none, downloading, installed, or installable. This makes the task of determining what to display in the view simple. Only values necessary for that state will be stored in the model.

pub fn view(&self) -> cosmic::Element<Message> { match self.package { Some(Package::Downloading { package, progress, total, time }) => { widget::text(format!("{package}} installing ({progress}/{total} {}s)", time.as_secs())) .into() } Some(Package::Installed { package }) => { widget::text(format!("{package} has already been installed")) .into() } Some(Package::Installable { package }) => { widget::button::text(format!("Install {package}")) .on_press(Message::Install) .into() } None => { widget::text("Select a package to install").into() } } }

You could similarly use this in an application to enable it to store data only for the currently-active page in the application.

struct App { page: Page, } enum Page { AboutPage(about::Page), ConfigPage(config::Page), TodoPage(todo::Page), } #[derive(Debug, Clone)] enum PageId { Config Todo, } #[derive(Debug, Clone)] enum Message { SetPage(PageId), ConfigPage(config::Message), TodoPage(todo::Message), } // ... fn view(&self) -> cosmic::Element<Message> { match self.page { Page::Todo(page) => page.view(), Page::Config(page) => page.view(), } } fn update(&mut self, message: Message) -> cosmic::Task<cosmic::Action<Message>> { match message { // Change the active page. Message::SetPage(id) => { match id { PageId::Config => { self.page = Page::Config(config::Page::new()); } PageId::Todo(page) => { self.page = Page::Todo(todo::Page::new()); return cosmic::task::future(async { Message::TodoPage(todo::load().await) }); } }; } // Apply the message only if the config page is active. Message::ConfigPage(message) => { if let Page::Config(ref mut page) = self.page { return page.update(message); } } // Apply the message only if the todo page is active. Message::TodoPage(message) => { if let Page::Todo(ref mut page) = self.page { return page.update(message); } } } }

Reducing Monomorphization

Rust uses monorphization to create multiple separate instances of types and functions that use generics—one for each type used. Although it improves performance over dynamic dispatch, it will increase compile times and binary size significantly if used excessively. If this is a concern, it will be important to keep elements across your application of the same message type. One way that you can reduce this is to pass a closure into your view and update functions to allow the caller to perform the conversion in advance.

pub fn view<Out>(&self, on_message: impl Fn(Message) -> Out) -> cosmic::Element<Out> { // Where new tasks will be input before being added to the task list. let new_task_input = widget::text_input("Write down a new task here", &self.new_task_input) .on_input(on_message(Message::NewInput)) .on_submit(on_message(Message::Add)); // Fold each enumerated task into a widget that is pushed to a scrollable column. let saved_tasks = self.tasks.iter() .enumerate() .fold(widget::column(), |column, (id, task)| { column.push( // A hypothetical widget created for this app crate::widget::task(task.as_str()) .on_remove(on_message(Message::Remove(id))) .on_input(|text| on_message(Message::EditInput(id, text))) .on_move_down(on_message(Message::MoveDown(id))) .on_move_up(on_message(Message::MoveUp(id))) .into() ) }) .apply(widget::scrollable); // Compose the above widgets into the column view. widget::column::with_capacity(2) .spacing(cosmic::theme::active().cosmic().spacing.space_l) .push(new_task_input) .push(saved_tasks) .into() }

However, since this function takes an impl type as an input paramter, it too will be monomorphized across different closure types used as the input. In some cases you might want to use a non-generic inner function where the inner function can be declared #[inline(never)]. In others, it may be easier to use dynamic dispatch with trait objects via the dyn keyword.

pub fn view<Out>(&self, on_message: &dyn Fn(Message) -> Out) -> cosmic::Element<Out>

Or

pub fn view<Out>(&self, on_message: Box<dyn Fn(Message) -> Out>) -> cosmic::Element<Out>

COSMIC Concepts

This section covers all aspects of libcosmic which are unique to the COSMIC toolkit.

All apps developed for COSMIC adhere to COSMIC's design language and interface guidelines. This includes common interface elements such as the header bar, navigation bar, and context drawer. Most of which are automatically derived for apps using the toolkit by default.

While it may be possible to ignore—and even override—them, apps developed for COSMIC should strive to utilize them in their designs. Consistent application of interface concepts improves accessibility of applications in the COSMIC ecosystem.

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()); } }

Panel Applets

COSMIC panel applets are built using the same libcosmic toolkit that desktop applications are built with. They operate as their own self-contained application processes with transparent headerless windows. No need to use a custom JavaScript API; or use a different shell toolkit; in a restrictive runtime environment.

The panel—which is actually a wayland compositor itself—reads its config file on startup to determine which applications to launch by their desktop entry names. Their position in the config determines where the panel will position their main window within itself. The popup windows that these applets create are forwarded to the host compositor to be displayed outside of the panel.

This architecture has many security benefits, in addition to easing the burden of development. Once you know how to build a COSMIC application, you can already create applets with only a few adjustments to the application. First of which is to run the application with cosmic::applet::run instead of cosmic::app::run.

Must enable the applet feature in libcosmic. May also want to remove wgpu to use a software renderer for lower memory usage.

cosmic::applet::run::<Power>(())

Next, you will define the view for main window, which will be used inside of the panel. There is a template provided by the cosmic::Core in case you wish to create a standard icon button. You only need to provide a message for toggling the popup created by the panel.

fn view(&self) -> cosmic::Element<Message> { self.core .applet .icon_button(&self.icon_name) .on_press_down(Message::TogglePopup) .into() }

In your update() method, you can create a popup window like so, which will destroy the popup if a popup is already active.

match message { Message::TogglePopup => { if let Some(p) = self.popup.take() { cosmic::iced::platform_specific::shell::commands::popup::destroy_popup(p) } else { let new_id = window::Id::unique(); self.popup.replace(new_id); let mut popup_settings = self.core.applet.get_popup_settings( self.core.main_window_id().unwrap(), new_id, Some((500, 500)), None, None, ); popup_settings.positioner.size_limits = Limits::NONE .min_width(100.0) .min_height(100.0) .max_height(400.0) .max_width(500.0); cosmic::iced::platform_specific::shell::commands::popup::get_popup(popup_settings) } } }

Now you can define the view of your popup window using Application::view_window, which takes a window ID as an input in the event that you have multiple windows to display views for. This particular example is from the power applet:

fn view_window(&self, id: window::Id) -> cosmic::Element<Message> { let Spacing { space_xxs, space_s, space_m, .. } = theme::active().cosmic().spacing; if matches!(self.popup, Some(p) if p == id) { let settings = menu_button(text::body(fl!("settings"))) .on_press(Message::Settings); let session = column![ menu_button( row![ text_icon("system-lock-screen-symbolic", 24), text::body(fl!("lock-screen")), Space::with_width(Length::Fill), text::body(fl!("lock-screen-shortcut")), ] .align_y(Alignment::Center) .spacing(space_xxs) ) .on_press(Message::Action(PowerAction::Lock)), menu_button( row![ text_icon("system-log-out-symbolic", 24), text::body(fl!("log-out")), Space::with_width(Length::Fill), text::body(fl!("log-out-shortcut")), ] .align_y(Alignment::Center) .spacing(space_xxs) ) .on_press(Message::Action(PowerAction::LogOut)), ]; let power = row![ power_buttons("system-suspend-symbolic", fl!("suspend")) .on_press(Message::Action(PowerAction::Suspend)), power_buttons("system-reboot-symbolic", fl!("restart")) .on_press(Message::Action(PowerAction::Restart)), power_buttons("system-shutdown-symbolic", fl!("shutdown")) .on_press(Message::Action(PowerAction::Shutdown)), ] .spacing(space_m) .padding([0, space_m]); let content = column![ settings, padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), session, padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), power ] .align_x(Alignment::Start) .padding([8, 0]); self.core .applet .popup_container(content) .max_height(400.) .max_width(500.) .into() } else { widget::text("").into() } }

You'll also want to use the applet style to get the transparent window background using Application::style.

fn style(&self) -> Option<cosmic::iced_runtime::Appearance> { Some(cosmic::applet::style()) }

Now all that's left is informing cosmic-settings about the existence of the applet by adding some keys to its desktop entry. See the power applet's desktop entry as an example, which defines NoDisplay=true, X-CosmicApplet=true, X-CosmicHoverPopup=Auto, X-CosmicHoverPopup=Auto, and X-OverflowPriority=10.

[Desktop Entry] Name=User Session Name[hu]=Felhasználói Munkamenet Name[pl]=Sesja użytkownika Type=Application Exec=cosmic-applet-power Terminal=false Categories=COSMIC; Keywords=COSMIC;Iced; # Translators: Do NOT translate or transliterate this text (this is an icon file name)! Icon=com.system76.CosmicAppletPower-symbolic StartupNotify=true NoDisplay=true X-CosmicApplet=true X-CosmicHoverPopup=Auto

Dialogs