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 Task
s 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 removewgpu
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