//! glimmer's libcosmic backend: the retained-tree ABI, with iced reading it. //! //! The edit half is libvidya's — integer node handles, string-keyed props, //! events queued and polled — so `glimmer-cosmic` is `glimmer-vidya` pointed //! at a different object. What changes is who owns the loop. //! //! egui lets its caller drive frames; iced does not. `cosmic::app::run` takes //! the main thread (winit insists) and returns when the window closes. So the //! arrangement is inverted: //! //! * `cosmic_run` blocks the process main thread inside libcosmic. //! * jolt reconciles on a worker thread, mutating the arena under a mutex. //! Nothing it does is visible until `cosmic_tree_commit`, which snapshots the //! tree and wakes iced — so a reconcile half-way through a patch is never //! painted, and a commit with no edits behind it costs nothing. //! * Interactions are queued, and `cosmic_wait` blocks the worker until there //! is one (or `cosmic_wake`, or a timeout), so an idle window burns no CPU on //! either side. //! //! Every call except `cosmic_run` may come from any thread. mod tree; pub use tree::{Node, Prop, Tree}; use std::collections::VecDeque; use std::ffi::{c_char, c_int}; use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; use std::sync::{Arc, Condvar, LazyLock, Mutex, MutexGuard}; use std::time::Duration; use cosmic::app::{Core, Task}; use cosmic::iced::futures::channel::mpsc; use cosmic::iced::futures::{Stream, StreamExt}; use cosmic::iced::{Alignment, Length, Subscription}; use cosmic::widget::{self, Column, Row}; use cosmic::{ApplicationExt, Element}; use jolt_abi::{borrowed, empty_str, guard, Scratch}; fn lock(m: &Mutex) -> MutexGuard<'_, T> { m.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) } // --- the arena --------------------------------------------------------------- struct Edits { tree: Tree, /// Set by every mutation, cleared by a commit that published it. dirty: bool, } static EDITS: LazyLock> = LazyLock::new(|| { Mutex::new(Edits { tree: Tree::default(), dirty: false, }) }); /// What `view` paints: the tree as of the last commit. static COMMITTED: LazyLock>> = LazyLock::new(Default::default); fn edit(f: impl FnOnce(&mut Tree) -> R) -> R { let mut e = lock(&EDITS); e.dirty = true; f(&mut e.tree) } fn read(f: impl FnOnce(&Tree) -> R) -> R { f(&lock(&EDITS).tree) } // --- events, towards jolt ---------------------------------------------------- struct Event { node: i32, name: &'static str, text: String, num: f64, } struct Inbox { queue: VecDeque, current: Option, woken: bool, } static INBOX: Mutex = Mutex::new(Inbox { queue: VecDeque::new(), current: None, woken: false, }); static BELL: Condvar = Condvar::new(); fn post(node: i32, name: &'static str, text: String, num: f64) { lock(&INBOX).queue.push_back(Event { node, name, text, num, }); BELL.notify_all(); } // --- wakes, towards iced ----------------------------------------------------- enum Wake { Tree, Quit, } static TO_APP: Mutex>> = Mutex::new(None); static QUIT_ASKED: AtomicBool = AtomicBool::new(false); static RAN: AtomicBool = AtomicBool::new(false); static CLOSED: AtomicBool = AtomicBool::new(false); fn tell_app(wake: Wake) { if let Some(tx) = lock(&TO_APP).as_ref() { let _ = tx.unbounded_send(wake); } } /// The subscription's stream. It opens with a `Tree` wake so a commit made /// between `init` and the subscription starting is not missed, and repeats a /// quit asked for before there was anyone to tell. fn wakes() -> impl Stream { let (tx, rx) = mpsc::unbounded(); let _ = tx.unbounded_send(Wake::Tree); if QUIT_ASKED.load(SeqCst) { let _ = tx.unbounded_send(Wake::Quit); } *lock(&TO_APP) = Some(tx); rx.map(|wake| match wake { Wake::Tree => Message::Tree, Wake::Quit => Message::Quit, }) } // --- the app ----------------------------------------------------------------- struct App { core: Core, tree: Arc, } #[derive(Clone, Debug)] enum Message { Tree, Quit, Click(i32), Toggled(i32, bool), Change(i32, String), Activate(i32), } impl App { /// A widget does not own its value: the new state goes into the arena and /// into what is painted, so a caller that ignores the event still sees a /// working control, and its next render is what settles it. fn write_back(&mut self, node: i32, key: &str, value: Prop) { edit(|t| t.set(node, key, value.clone())); Arc::make_mut(&mut self.tree).set(node, key, value); } } impl cosmic::Application for App { type Executor = cosmic::executor::Default; type Flags = String; type Message = Message; const APP_ID: &'static str = "dev.jolt.Glimmer"; fn core(&self) -> &Core { &self.core } fn core_mut(&mut self) -> &mut Core { &mut self.core } fn init(core: Core, title: String) -> (Self, Task) { let mut app = App { core, tree: lock(&COMMITTED).clone(), }; // libcosmic's `wayland` feature brings `multi-window` with it, which // makes a window title a per-window thing. app.set_header_title(title.clone()); let task = match app.core.main_window_id() { Some(id) => app.set_window_title(title, id), None => Task::none(), }; (app, task) } fn subscription(&self) -> Subscription { Subscription::run(wakes) } fn update(&mut self, message: Message) -> Task { match message { Message::Tree => self.tree = lock(&COMMITTED).clone(), Message::Quit => return cosmic::iced::exit(), Message::Click(node) => post(node, "click", String::new(), 0.0), Message::Toggled(node, on) => { self.write_back(node, "active", Prop::Bool(on)); post(node, "toggled", String::new(), f64::from(u8::from(on))); } Message::Change(node, text) => { self.write_back(node, "text", Prop::Str(text.clone())); post(node, "change", text, 0.0); } Message::Activate(node) => post(node, "activate", String::new(), 0.0), } Task::none() } fn view(&self) -> Element<'_, Message> { let tree = &*self.tree; element(tree, tree.root_id(), true) } } /// One node and everything under it, as widgets. `enabled` is inherited: an /// insensitive container takes its whole subtree out of interaction. fn element(t: &Tree, id: i32, enabled: bool) -> Element<'_, Message> { let Some(n) = t.get(id) else { return Column::new().into(); }; let enabled = enabled && n.bool("sensitive") != Some(false); let spacing = n.num("spacing").unwrap_or(8.0) as f32; let children = || n.children.iter().map(move |c| element(t, *c, enabled)); let el: Element<'_, Message> = match n.tag.as_str() { "window" => Column::with_children(children()) .width(Length::Fill) .height(Length::Fill) .into(), "box" => { let margin = n.num("margin").unwrap_or(0.0) as f32; if n.str("orientation") == "horizontal" { Row::with_children(children()) .spacing(spacing) .padding(margin) .align_y(Alignment::Center) .into() } else { Column::with_children(children()) .spacing(spacing) .padding(margin) .into() } } "page" => { let column = Column::with_children(children()) .spacing(spacing) .padding(24) .width(Length::Fill); let mut inner = widget::container(column).width(Length::Fill); if let Some(max) = n.num("max-width") { inner = inner.max_width(max as f32); } widget::scrollable(widget::container(inner).center_x(Length::Fill)) .height(Length::Fill) .into() } "card" | "frame" => { let mut column = Column::new().spacing(spacing); if n.tag == "frame" && !n.label().is_empty() { column = column.push(widget::text::heading(n.label())); } widget::container(column.extend(children())) .padding(16) .width(Length::Fill) .class(cosmic::theme::Container::Card) .into() } "scroll" => widget::scrollable(Column::with_children(children()).spacing(spacing)).into(), "label" => widget::text::body(n.label()).into(), "title" => widget::text::title3(n.label()).into(), "title-2" => widget::text::title4(n.label()).into(), "dim-label" => widget::text::caption(n.label()).into(), "button" => { let button = match n.str("kind") { "primary" => widget::button::suggested(n.label()), "destructive" => widget::button::destructive(n.label()), _ => widget::button::standard(n.label()), }; button .on_press_maybe(enabled.then_some(Message::Click(id))) .into() } "checkbutton" => { let mut check = widget::checkbox(n.bool("active").unwrap_or(false)).label(n.label()); if enabled { check = check.on_toggle(move |on| Message::Toggled(id, on)); } check.into() } "entry" => { let mut entry = widget::text_input(n.str("placeholder"), n.str("text")); if enabled { entry = entry .on_input(move |text| Message::Change(id, text)) .on_submit(move |_| Message::Activate(id)); } entry.into() } "separator" => widget::divider::horizontal::default().into(), "spacer" => { let size = n.num("size").unwrap_or(8.0) as f32; widget::Space::new().width(size).height(size).into() } "progress" => { let bar = widget::progress_bar::determinate_linear(n.num("value").unwrap_or(0.0) as f32); if n.label().is_empty() { bar.into() } else { Column::new() .spacing(4) .push(widget::text::caption(n.label())) .push(bar) .into() } } // Kept rather than refused, as in libvidya: a tag this backend has not // grown yet still shows its children. _ => Column::with_children(children()).spacing(spacing).into(), }; match n.num("width-request") { Some(width) => widget::container(el).width(width as f32).into(), None => el, } } // --- the C ABI: the loop ------------------------------------------------------- static TITLE: Mutex = Mutex::new(String::new()); /// The window's title, read when `cosmic_run` opens it. A call of its own /// because jolt will not pass a string to a `:blocking` foreign procedure, and /// `cosmic_run` has to be one. /// /// # Safety /// `title` is null or a NUL-terminated string. #[no_mangle] pub unsafe extern "C" fn cosmic_set_title(title: *const c_char) { let title = borrowed(title); guard((), || *lock(&TITLE) = title) } /// Open the window and run libcosmic until it closes. Blocks; call it on the /// process main thread. `mode` is 0 for the system theme, 1 dark, 2 light. /// /// Answers 0 on a clean exit, 1 on an error, 2 when a window was already run /// in this process — winit's event loop cannot be made twice. #[no_mangle] pub extern "C" fn cosmic_run(width: c_int, height: c_int, mode: c_int) -> c_int { let status = guard(1, || { let title = lock(&TITLE).clone(); if RAN.swap(true, SeqCst) { log::error!("jolt-cosmic: a window already ran in this process"); return 2; } let size = cosmic::iced::Size::new(width.max(1) as f32, height.max(1) as f32); let mut settings = cosmic::app::Settings::default().size(size); match mode { 1 => settings = settings.theme(cosmic::Theme::dark()), 2 => settings = settings.theme(cosmic::Theme::light()), _ => {} } match cosmic::app::run::(settings, title) { Ok(()) => 0, Err(err) => { eprintln!("jolt-cosmic: {err}"); 1 } } }); // Outside the guard, so a panic in libcosmic still releases the worker. *lock(&TO_APP) = None; CLOSED.store(true, SeqCst); BELL.notify_all(); status } /// 1 once `cosmic_run` has returned. #[no_mangle] pub extern "C" fn cosmic_should_close() -> c_int { c_int::from(CLOSED.load(SeqCst)) } /// Close the window. Asked before the window exists, it closes on opening. #[no_mangle] pub extern "C" fn cosmic_quit() { guard((), || { QUIT_ASKED.store(true, SeqCst); tell_app(Wake::Quit); }) } /// Publish the edits since the last commit. Answers 1 when there were any. #[no_mangle] pub extern "C" fn cosmic_tree_commit() -> c_int { guard(0, || { let snapshot = { let mut e = lock(&EDITS); if !e.dirty { return 0; } e.dirty = false; Arc::new(e.tree.clone()) }; *lock(&COMMITTED) = snapshot; tell_app(Wake::Tree); 1 }) } /// Block up to `timeout_ms` for an event, a `cosmic_wake`, or the window /// closing. Answers 1 when an event is waiting. #[no_mangle] pub extern "C" fn cosmic_wait(timeout_ms: c_int) -> c_int { guard(0, || { let timeout = Duration::from_millis(timeout_ms.max(0) as u64); let (mut inbox, _) = BELL .wait_timeout_while(lock(&INBOX), timeout, |i| { i.queue.is_empty() && !i.woken && !CLOSED.load(SeqCst) }) .unwrap_or_else(|poisoned| poisoned.into_inner()); inbox.woken = false; c_int::from(!inbox.queue.is_empty()) }) } /// Cut a `cosmic_wait` short — for work queued for the worker from elsewhere. #[no_mangle] pub extern "C" fn cosmic_wake() { guard((), || { lock(&INBOX).woken = true; BELL.notify_all(); }) } // --- the C ABI: events ----------------------------------------------------------- static EVENT_NAME: Scratch = Scratch::new(); static EVENT_TEXT: Scratch = Scratch::new(); /// Dequeue one event; 1 while there was one. The accessors describe it. #[no_mangle] pub extern "C" fn cosmic_tree_poll_event() -> c_int { guard(0, || { let mut inbox = lock(&INBOX); let next = inbox.queue.pop_front(); let got = next.is_some(); inbox.current = next; c_int::from(got) }) } #[no_mangle] pub extern "C" fn cosmic_tree_event_node() -> c_int { guard(0, || lock(&INBOX).current.as_ref().map_or(0, |e| e.node)) } #[no_mangle] pub extern "C" fn cosmic_tree_event_name() -> *const c_char { guard(empty_str(), || { EVENT_NAME.lend(lock(&INBOX).current.as_ref().map_or("", |e| e.name)) }) } #[no_mangle] pub extern "C" fn cosmic_tree_event_text() -> *const c_char { guard(empty_str(), || { let text = lock(&INBOX) .current .as_ref() .map(|e| e.text.clone()) .unwrap_or_default(); EVENT_TEXT.lend(text) }) } #[no_mangle] pub extern "C" fn cosmic_tree_event_num() -> f64 { guard(0.0, || lock(&INBOX).current.as_ref().map_or(0.0, |e| e.num)) } // --- the C ABI: nodes -------------------------------------------------------------- static PROPS: Scratch = Scratch::new(); static DUMP: Scratch = Scratch::new(); #[no_mangle] pub extern "C" fn cosmic_tree_root() -> c_int { guard(0, || edit(Tree::root)) } /// # Safety /// `tag` is null or a NUL-terminated string. #[no_mangle] pub unsafe extern "C" fn cosmic_node_new(tag: *const c_char) -> c_int { let tag = borrowed(tag); guard(0, || edit(|t| t.new_node(&tag))) } #[no_mangle] pub extern "C" fn cosmic_node_free(node: c_int) { guard((), || edit(|t| t.free(node))) } #[no_mangle] pub extern "C" fn cosmic_node_exists(node: c_int) -> c_int { guard(0, || c_int::from(read(|t| t.exists(node)))) } /// # Safety /// `key` and `value` are null or NUL-terminated strings. #[no_mangle] pub unsafe extern "C" fn cosmic_node_set_str( node: c_int, key: *const c_char, value: *const c_char, ) { let (key, value) = (borrowed(key), borrowed(value)); guard((), || edit(|t| t.set(node, &key, Prop::Str(value)))) } /// # Safety /// `key` is null or a NUL-terminated string. #[no_mangle] pub unsafe extern "C" fn cosmic_node_set_num(node: c_int, key: *const c_char, value: f64) { let key = borrowed(key); guard((), || edit(|t| t.set(node, &key, Prop::Num(value)))) } /// # Safety /// `key` is null or a NUL-terminated string. #[no_mangle] pub unsafe extern "C" fn cosmic_node_set_bool(node: c_int, key: *const c_char, value: c_int) { let key = borrowed(key); guard((), || edit(|t| t.set(node, &key, Prop::Bool(value != 0)))) } #[no_mangle] pub extern "C" fn cosmic_node_clear_props(node: c_int) { guard((), || edit(|t| t.clear_props(node))) } #[no_mangle] pub extern "C" fn cosmic_node_tag(node: c_int) -> *const c_char { guard(empty_str(), || { PROPS.lend(read(|t| { t.get(node).map(|n| n.tag.clone()).unwrap_or_default() })) }) } #[no_mangle] pub extern "C" fn cosmic_node_child_count(node: c_int) -> c_int { guard(0, || { read(|t| t.get(node).map_or(0, |n| n.children.len() as c_int)) }) } #[no_mangle] pub extern "C" fn cosmic_node_child_at(node: c_int, index: c_int) -> c_int { guard(0, || { read(|t| { t.get(node) .and_then(|n| n.children.get(usize::try_from(index).ok()?).copied()) .unwrap_or(0) }) }) } #[no_mangle] pub extern "C" fn cosmic_node_append(parent: c_int, child: c_int) -> c_int { guard(0, || c_int::from(edit(|t| t.append(parent, child)))) } /// Unparents AND frees `child` with everything under it. #[no_mangle] pub extern "C" fn cosmic_node_remove(parent: c_int, child: c_int) { guard((), || edit(|t| t.remove(parent, child))) } #[no_mangle] pub extern "C" fn cosmic_node_insert_after(parent: c_int, child: c_int, sibling: c_int) -> c_int { guard(0, || { c_int::from(edit(|t| t.insert_after(parent, child, sibling))) }) } #[no_mangle] pub extern "C" fn cosmic_node_replace(parent: c_int, old_child: c_int, new_child: c_int) -> c_int { guard(0, || { c_int::from(edit(|t| t.replace(parent, old_child, new_child))) }) } /// The subtree at `node` as hiccup; 0 is the root. #[no_mangle] pub extern "C" fn cosmic_tree_dump(node: c_int) -> *const c_char { guard(empty_str(), || { DUMP.lend(read(|t| { let id = if node == 0 { t.root_id() } else { node }; t.dump(id) })) }) }