nandi/jolt-nativepublic Fork 0
6a3304ddddcc7d3e9486b470fea5933a1f81f8e8
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

lib.rs · 613 lines · 19.1 KBRust Blame HistoryRaw
glimmer-cosmic: a libcosmic backend for glimmer (spike) 6a3304d nandi 9d ago1//! glimmer's libcosmic backend: the retained-tree ABI, with iced reading it.
2//!
3//! The edit half is libvidya's — integer node handles, string-keyed props,
4//! events queued and polled — so `glimmer-cosmic` is `glimmer-vidya` pointed
5//! at a different object. What changes is who owns the loop.
6//!
7//! egui lets its caller drive frames; iced does not. `cosmic::app::run` takes
8//! the main thread (winit insists) and returns when the window closes. So the
9//! arrangement is inverted:
10//!
11//! * `cosmic_run` blocks the process main thread inside libcosmic.
12//! * jolt reconciles on a worker thread, mutating the arena under a mutex.
13//! Nothing it does is visible until `cosmic_tree_commit`, which snapshots the
14//! tree and wakes iced — so a reconcile half-way through a patch is never
15//! painted, and a commit with no edits behind it costs nothing.
16//! * Interactions are queued, and `cosmic_wait` blocks the worker until there
17//! is one (or `cosmic_wake`, or a timeout), so an idle window burns no CPU on
18//! either side.
19//!
20//! Every call except `cosmic_run` may come from any thread.
21
22mod tree;
23
24pub use tree::{Node, Prop, Tree};
25
26use std::collections::VecDeque;
27use std::ffi::{c_char, c_int};
28use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
29use std::sync::{Arc, Condvar, LazyLock, Mutex, MutexGuard};
30use std::time::Duration;
31
32use cosmic::app::{Core, Task};
33use cosmic::iced::futures::channel::mpsc;
34use cosmic::iced::futures::{Stream, StreamExt};
35use cosmic::iced::{Alignment, Length, Subscription};
36use cosmic::widget::{self, Column, Row};
37use cosmic::{ApplicationExt, Element};
38use jolt_abi::{borrowed, empty_str, guard, Scratch};
39
40fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
41 m.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
42}
43
44// --- the arena ---------------------------------------------------------------
45
46struct Edits {
47 tree: Tree,
48 /// Set by every mutation, cleared by a commit that published it.
49 dirty: bool,
50}
51
52static EDITS: LazyLock<Mutex<Edits>> = LazyLock::new(|| {
53 Mutex::new(Edits {
54 tree: Tree::default(),
55 dirty: false,
56 })
57});
58
59/// What `view` paints: the tree as of the last commit.
60static COMMITTED: LazyLock<Mutex<Arc<Tree>>> = LazyLock::new(Default::default);
61
62fn edit<R>(f: impl FnOnce(&mut Tree) -> R) -> R {
63 let mut e = lock(&EDITS);
64 e.dirty = true;
65 f(&mut e.tree)
66}
67
68fn read<R>(f: impl FnOnce(&Tree) -> R) -> R {
69 f(&lock(&EDITS).tree)
70}
71
72// --- events, towards jolt ----------------------------------------------------
73
74struct Event {
75 node: i32,
76 name: &'static str,
77 text: String,
78 num: f64,
79}
80
81struct Inbox {
82 queue: VecDeque<Event>,
83 current: Option<Event>,
84 woken: bool,
85}
86
87static INBOX: Mutex<Inbox> = Mutex::new(Inbox {
88 queue: VecDeque::new(),
89 current: None,
90 woken: false,
91});
92static BELL: Condvar = Condvar::new();
93
94fn post(node: i32, name: &'static str, text: String, num: f64) {
95 lock(&INBOX).queue.push_back(Event {
96 node,
97 name,
98 text,
99 num,
100 });
101 BELL.notify_all();
102}
103
104// --- wakes, towards iced -----------------------------------------------------
105
106enum Wake {
107 Tree,
108 Quit,
109}
110
111static TO_APP: Mutex<Option<mpsc::UnboundedSender<Wake>>> = Mutex::new(None);
112static QUIT_ASKED: AtomicBool = AtomicBool::new(false);
113static RAN: AtomicBool = AtomicBool::new(false);
114static CLOSED: AtomicBool = AtomicBool::new(false);
115
116fn tell_app(wake: Wake) {
117 if let Some(tx) = lock(&TO_APP).as_ref() {
118 let _ = tx.unbounded_send(wake);
119 }
120}
121
122/// The subscription's stream. It opens with a `Tree` wake so a commit made
123/// between `init` and the subscription starting is not missed, and repeats a
124/// quit asked for before there was anyone to tell.
125fn wakes() -> impl Stream<Item = Message> {
126 let (tx, rx) = mpsc::unbounded();
127 let _ = tx.unbounded_send(Wake::Tree);
128 if QUIT_ASKED.load(SeqCst) {
129 let _ = tx.unbounded_send(Wake::Quit);
130 }
131 *lock(&TO_APP) = Some(tx);
132 rx.map(|wake| match wake {
133 Wake::Tree => Message::Tree,
134 Wake::Quit => Message::Quit,
135 })
136}
137
138// --- the app -----------------------------------------------------------------
139
140struct App {
141 core: Core,
142 tree: Arc<Tree>,
143}
144
145#[derive(Clone, Debug)]
146enum Message {
147 Tree,
148 Quit,
149 Click(i32),
150 Toggled(i32, bool),
151 Change(i32, String),
152 Activate(i32),
153}
154
155impl App {
156 /// A widget does not own its value: the new state goes into the arena and
157 /// into what is painted, so a caller that ignores the event still sees a
158 /// working control, and its next render is what settles it.
159 fn write_back(&mut self, node: i32, key: &str, value: Prop) {
160 edit(|t| t.set(node, key, value.clone()));
161 Arc::make_mut(&mut self.tree).set(node, key, value);
162 }
163}
164
165impl cosmic::Application for App {
166 type Executor = cosmic::executor::Default;
167 type Flags = String;
168 type Message = Message;
169 const APP_ID: &'static str = "dev.jolt.Glimmer";
170
171 fn core(&self) -> &Core {
172 &self.core
173 }
174
175 fn core_mut(&mut self) -> &mut Core {
176 &mut self.core
177 }
178
179 fn init(core: Core, title: String) -> (Self, Task<Message>) {
180 let mut app = App {
181 core,
182 tree: lock(&COMMITTED).clone(),
183 };
184 // libcosmic's `wayland` feature brings `multi-window` with it, which
185 // makes a window title a per-window thing.
186 app.set_header_title(title.clone());
187 let task = match app.core.main_window_id() {
188 Some(id) => app.set_window_title(title, id),
189 None => Task::none(),
190 };
191 (app, task)
192 }
193
194 fn subscription(&self) -> Subscription<Message> {
195 Subscription::run(wakes)
196 }
197
198 fn update(&mut self, message: Message) -> Task<Message> {
199 match message {
200 Message::Tree => self.tree = lock(&COMMITTED).clone(),
201 Message::Quit => return cosmic::iced::exit(),
202 Message::Click(node) => post(node, "click", String::new(), 0.0),
203 Message::Toggled(node, on) => {
204 self.write_back(node, "active", Prop::Bool(on));
205 post(node, "toggled", String::new(), f64::from(u8::from(on)));
206 }
207 Message::Change(node, text) => {
208 self.write_back(node, "text", Prop::Str(text.clone()));
209 post(node, "change", text, 0.0);
210 }
211 Message::Activate(node) => post(node, "activate", String::new(), 0.0),
212 }
213 Task::none()
214 }
215
216 fn view(&self) -> Element<'_, Message> {
217 let tree = &*self.tree;
218 element(tree, tree.root_id(), true)
219 }
220}
221
222/// One node and everything under it, as widgets. `enabled` is inherited: an
223/// insensitive container takes its whole subtree out of interaction.
224fn element(t: &Tree, id: i32, enabled: bool) -> Element<'_, Message> {
225 let Some(n) = t.get(id) else {
226 return Column::new().into();
227 };
228 let enabled = enabled && n.bool("sensitive") != Some(false);
229 let spacing = n.num("spacing").unwrap_or(8.0) as f32;
230 let children = || n.children.iter().map(move |c| element(t, *c, enabled));
231
232 let el: Element<'_, Message> = match n.tag.as_str() {
233 "window" => Column::with_children(children())
234 .width(Length::Fill)
235 .height(Length::Fill)
236 .into(),
237 "box" => {
238 let margin = n.num("margin").unwrap_or(0.0) as f32;
239 if n.str("orientation") == "horizontal" {
240 Row::with_children(children())
241 .spacing(spacing)
242 .padding(margin)
243 .align_y(Alignment::Center)
244 .into()
245 } else {
246 Column::with_children(children())
247 .spacing(spacing)
248 .padding(margin)
249 .into()
250 }
251 }
252 "page" => {
253 let column = Column::with_children(children())
254 .spacing(spacing)
255 .padding(24)
256 .width(Length::Fill);
257 let mut inner = widget::container(column).width(Length::Fill);
258 if let Some(max) = n.num("max-width") {
259 inner = inner.max_width(max as f32);
260 }
261 widget::scrollable(widget::container(inner).center_x(Length::Fill))
262 .height(Length::Fill)
263 .into()
264 }
265 "card" | "frame" => {
266 let mut column = Column::new().spacing(spacing);
267 if n.tag == "frame" && !n.label().is_empty() {
268 column = column.push(widget::text::heading(n.label()));
269 }
270 widget::container(column.extend(children()))
271 .padding(16)
272 .width(Length::Fill)
273 .class(cosmic::theme::Container::Card)
274 .into()
275 }
276 "scroll" => widget::scrollable(Column::with_children(children()).spacing(spacing)).into(),
277 "label" => widget::text::body(n.label()).into(),
278 "title" => widget::text::title3(n.label()).into(),
279 "title-2" => widget::text::title4(n.label()).into(),
280 "dim-label" => widget::text::caption(n.label()).into(),
281 "button" => {
282 let button = match n.str("kind") {
283 "primary" => widget::button::suggested(n.label()),
284 "destructive" => widget::button::destructive(n.label()),
285 _ => widget::button::standard(n.label()),
286 };
287 button
288 .on_press_maybe(enabled.then_some(Message::Click(id)))
289 .into()
290 }
291 "checkbutton" => {
292 let mut check = widget::checkbox(n.bool("active").unwrap_or(false)).label(n.label());
293 if enabled {
294 check = check.on_toggle(move |on| Message::Toggled(id, on));
295 }
296 check.into()
297 }
298 "entry" => {
299 let mut entry = widget::text_input(n.str("placeholder"), n.str("text"));
300 if enabled {
301 entry = entry
302 .on_input(move |text| Message::Change(id, text))
303 .on_submit(move |_| Message::Activate(id));
304 }
305 entry.into()
306 }
307 "separator" => widget::divider::horizontal::default().into(),
308 "spacer" => {
309 let size = n.num("size").unwrap_or(8.0) as f32;
310 widget::Space::new().width(size).height(size).into()
311 }
312 "progress" => {
313 let bar =
314 widget::progress_bar::determinate_linear(n.num("value").unwrap_or(0.0) as f32);
315 if n.label().is_empty() {
316 bar.into()
317 } else {
318 Column::new()
319 .spacing(4)
320 .push(widget::text::caption(n.label()))
321 .push(bar)
322 .into()
323 }
324 }
325 // Kept rather than refused, as in libvidya: a tag this backend has not
326 // grown yet still shows its children.
327 _ => Column::with_children(children()).spacing(spacing).into(),
328 };
329
330 match n.num("width-request") {
331 Some(width) => widget::container(el).width(width as f32).into(),
332 None => el,
333 }
334}
335
336// --- the C ABI: the loop -------------------------------------------------------
337
338static TITLE: Mutex<String> = Mutex::new(String::new());
339
340/// The window's title, read when `cosmic_run` opens it. A call of its own
341/// because jolt will not pass a string to a `:blocking` foreign procedure, and
342/// `cosmic_run` has to be one.
343///
344/// # Safety
345/// `title` is null or a NUL-terminated string.
346#[no_mangle]
347pub unsafe extern "C" fn cosmic_set_title(title: *const c_char) {
348 let title = borrowed(title);
349 guard((), || *lock(&TITLE) = title)
350}
351
352/// Open the window and run libcosmic until it closes. Blocks; call it on the
353/// process main thread. `mode` is 0 for the system theme, 1 dark, 2 light.
354///
355/// Answers 0 on a clean exit, 1 on an error, 2 when a window was already run
356/// in this process — winit's event loop cannot be made twice.
357#[no_mangle]
358pub extern "C" fn cosmic_run(width: c_int, height: c_int, mode: c_int) -> c_int {
359 let status = guard(1, || {
360 let title = lock(&TITLE).clone();
361 if RAN.swap(true, SeqCst) {
362 log::error!("jolt-cosmic: a window already ran in this process");
363 return 2;
364 }
365 let size = cosmic::iced::Size::new(width.max(1) as f32, height.max(1) as f32);
366 let mut settings = cosmic::app::Settings::default().size(size);
367 match mode {
368 1 => settings = settings.theme(cosmic::Theme::dark()),
369 2 => settings = settings.theme(cosmic::Theme::light()),
370 _ => {}
371 }
372 match cosmic::app::run::<App>(settings, title) {
373 Ok(()) => 0,
374 Err(err) => {
375 eprintln!("jolt-cosmic: {err}");
376 1
377 }
378 }
379 });
380 // Outside the guard, so a panic in libcosmic still releases the worker.
381 *lock(&TO_APP) = None;
382 CLOSED.store(true, SeqCst);
383 BELL.notify_all();
384 status
385}
386
387/// 1 once `cosmic_run` has returned.
388#[no_mangle]
389pub extern "C" fn cosmic_should_close() -> c_int {
390 c_int::from(CLOSED.load(SeqCst))
391}
392
393/// Close the window. Asked before the window exists, it closes on opening.
394#[no_mangle]
395pub extern "C" fn cosmic_quit() {
396 guard((), || {
397 QUIT_ASKED.store(true, SeqCst);
398 tell_app(Wake::Quit);
399 })
400}
401
402/// Publish the edits since the last commit. Answers 1 when there were any.
403#[no_mangle]
404pub extern "C" fn cosmic_tree_commit() -> c_int {
405 guard(0, || {
406 let snapshot = {
407 let mut e = lock(&EDITS);
408 if !e.dirty {
409 return 0;
410 }
411 e.dirty = false;
412 Arc::new(e.tree.clone())
413 };
414 *lock(&COMMITTED) = snapshot;
415 tell_app(Wake::Tree);
416 1
417 })
418}
419
420/// Block up to `timeout_ms` for an event, a `cosmic_wake`, or the window
421/// closing. Answers 1 when an event is waiting.
422#[no_mangle]
423pub extern "C" fn cosmic_wait(timeout_ms: c_int) -> c_int {
424 guard(0, || {
425 let timeout = Duration::from_millis(timeout_ms.max(0) as u64);
426 let (mut inbox, _) = BELL
427 .wait_timeout_while(lock(&INBOX), timeout, |i| {
428 i.queue.is_empty() && !i.woken && !CLOSED.load(SeqCst)
429 })
430 .unwrap_or_else(|poisoned| poisoned.into_inner());
431 inbox.woken = false;
432 c_int::from(!inbox.queue.is_empty())
433 })
434}
435
436/// Cut a `cosmic_wait` short — for work queued for the worker from elsewhere.
437#[no_mangle]
438pub extern "C" fn cosmic_wake() {
439 guard((), || {
440 lock(&INBOX).woken = true;
441 BELL.notify_all();
442 })
443}
444
445// --- the C ABI: events -----------------------------------------------------------
446
447static EVENT_NAME: Scratch = Scratch::new();
448static EVENT_TEXT: Scratch = Scratch::new();
449
450/// Dequeue one event; 1 while there was one. The accessors describe it.
451#[no_mangle]
452pub extern "C" fn cosmic_tree_poll_event() -> c_int {
453 guard(0, || {
454 let mut inbox = lock(&INBOX);
455 let next = inbox.queue.pop_front();
456 let got = next.is_some();
457 inbox.current = next;
458 c_int::from(got)
459 })
460}
461
462#[no_mangle]
463pub extern "C" fn cosmic_tree_event_node() -> c_int {
464 guard(0, || lock(&INBOX).current.as_ref().map_or(0, |e| e.node))
465}
466
467#[no_mangle]
468pub extern "C" fn cosmic_tree_event_name() -> *const c_char {
469 guard(empty_str(), || {
470 EVENT_NAME.lend(lock(&INBOX).current.as_ref().map_or("", |e| e.name))
471 })
472}
473
474#[no_mangle]
475pub extern "C" fn cosmic_tree_event_text() -> *const c_char {
476 guard(empty_str(), || {
477 let text = lock(&INBOX)
478 .current
479 .as_ref()
480 .map(|e| e.text.clone())
481 .unwrap_or_default();
482 EVENT_TEXT.lend(text)
483 })
484}
485
486#[no_mangle]
487pub extern "C" fn cosmic_tree_event_num() -> f64 {
488 guard(0.0, || lock(&INBOX).current.as_ref().map_or(0.0, |e| e.num))
489}
490
491// --- the C ABI: nodes --------------------------------------------------------------
492
493static PROPS: Scratch = Scratch::new();
494static DUMP: Scratch = Scratch::new();
495
496#[no_mangle]
497pub extern "C" fn cosmic_tree_root() -> c_int {
498 guard(0, || edit(Tree::root))
499}
500
501/// # Safety
502/// `tag` is null or a NUL-terminated string.
503#[no_mangle]
504pub unsafe extern "C" fn cosmic_node_new(tag: *const c_char) -> c_int {
505 let tag = borrowed(tag);
506 guard(0, || edit(|t| t.new_node(&tag)))
507}
508
509#[no_mangle]
510pub extern "C" fn cosmic_node_free(node: c_int) {
511 guard((), || edit(|t| t.free(node)))
512}
513
514#[no_mangle]
515pub extern "C" fn cosmic_node_exists(node: c_int) -> c_int {
516 guard(0, || c_int::from(read(|t| t.exists(node))))
517}
518
519/// # Safety
520/// `key` and `value` are null or NUL-terminated strings.
521#[no_mangle]
522pub unsafe extern "C" fn cosmic_node_set_str(
523 node: c_int,
524 key: *const c_char,
525 value: *const c_char,
526) {
527 let (key, value) = (borrowed(key), borrowed(value));
528 guard((), || edit(|t| t.set(node, &key, Prop::Str(value))))
529}
530
531/// # Safety
532/// `key` is null or a NUL-terminated string.
533#[no_mangle]
534pub unsafe extern "C" fn cosmic_node_set_num(node: c_int, key: *const c_char, value: f64) {
535 let key = borrowed(key);
536 guard((), || edit(|t| t.set(node, &key, Prop::Num(value))))
537}
538
539/// # Safety
540/// `key` is null or a NUL-terminated string.
541#[no_mangle]
542pub unsafe extern "C" fn cosmic_node_set_bool(node: c_int, key: *const c_char, value: c_int) {
543 let key = borrowed(key);
544 guard((), || edit(|t| t.set(node, &key, Prop::Bool(value != 0))))
545}
546
547#[no_mangle]
548pub extern "C" fn cosmic_node_clear_props(node: c_int) {
549 guard((), || edit(|t| t.clear_props(node)))
550}
551
552#[no_mangle]
553pub extern "C" fn cosmic_node_tag(node: c_int) -> *const c_char {
554 guard(empty_str(), || {
555 PROPS.lend(read(|t| {
556 t.get(node).map(|n| n.tag.clone()).unwrap_or_default()
557 }))
558 })
559}
560
561#[no_mangle]
562pub extern "C" fn cosmic_node_child_count(node: c_int) -> c_int {
563 guard(0, || {
564 read(|t| t.get(node).map_or(0, |n| n.children.len() as c_int))
565 })
566}
567
568#[no_mangle]
569pub extern "C" fn cosmic_node_child_at(node: c_int, index: c_int) -> c_int {
570 guard(0, || {
571 read(|t| {
572 t.get(node)
573 .and_then(|n| n.children.get(usize::try_from(index).ok()?).copied())
574 .unwrap_or(0)
575 })
576 })
577}
578
579#[no_mangle]
580pub extern "C" fn cosmic_node_append(parent: c_int, child: c_int) -> c_int {
581 guard(0, || c_int::from(edit(|t| t.append(parent, child))))
582}
583
584/// Unparents AND frees `child` with everything under it.
585#[no_mangle]
586pub extern "C" fn cosmic_node_remove(parent: c_int, child: c_int) {
587 guard((), || edit(|t| t.remove(parent, child)))
588}
589
590#[no_mangle]
591pub extern "C" fn cosmic_node_insert_after(parent: c_int, child: c_int, sibling: c_int) -> c_int {
592 guard(0, || {
593 c_int::from(edit(|t| t.insert_after(parent, child, sibling)))
594 })
595}
596
597#[no_mangle]
598pub extern "C" fn cosmic_node_replace(parent: c_int, old_child: c_int, new_child: c_int) -> c_int {
599 guard(0, || {
600 c_int::from(edit(|t| t.replace(parent, old_child, new_child)))
601 })
602}
603
604/// The subtree at `node` as hiccup; 0 is the root.
605#[no_mangle]
606pub extern "C" fn cosmic_tree_dump(node: c_int) -> *const c_char {
607 guard(empty_str(), || {
608 DUMP.lend(read(|t| {
609 let id = if node == 0 { t.root_id() } else { node };
610 t.dump(id)
611 }))
612 })
613}