nandi/cosmicnimpublic Fork 0
18ec8fd
Commits
Clone
git clone https://git.rickub.com/nandi/cosmicnim.git
git clone ssh://git@rickub.com/nandi/cosmicnim.git

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

Let the host describe the widget tree, not just the label

The C surface was one text label and two buttons, hardcoded in view().
Anything past a counter needed a new export per widget.

Instead give the host an immediate-mode builder: libcosmic calls back
through on_view each frame with an opaque handle, and the host issues
cosmic_column/row/text/button calls to describe its tree. Rust keeps
ownership of all widget memory; the host only ever issues calls. Buttons
carry a host-chosen id that comes back through on_press.

Because the tree is rebuilt per frame it can depend on state, so the demo
now disables Reset at zero and grows a history line as it goes.

Also adds a justfile for the build/run/fetch tasks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T10:09:32-07:00 Browse files
18ec8fd parent: 4bb6568
modified README.md +48 -14
@@ -5,40 +5,74 @@
55 libcosmic's API is generics, traits and closures, so it has no C ABI of its own —
66 building libcosmic itself as a `cdylib` exports nothing. The `cosmic_ffi` crate
77 therefore *defines* the C surface: it depends on libcosmic as a normal Rust
8-crate and exports one function.
8+crate and exports a handful of functions.
99
1010 ```
1111 cosmic_ffi/ Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h
1212 nim/ Nim bindings (cosmic.nim) and the demo app (app.nim)
13+justfile build / run / fetch tasks
1314 ```
1415
1516 ## The contract
1617
17-Rust owns the widget tree; the host owns the application state. Each button
18-press is handed back through `on_press`, which writes the next label into the
19-buffer it is given.
18+The host owns the application state *and* the shape of the window. Once per
19+frame libcosmic calls back through `on_view` with an opaque builder, and the
20+host describes its widget tree by calling the builder functions. Widget memory
21+never crosses the boundary — the host only ever issues calls, and the builder
22+handle is dead the moment `on_view` returns.
23+
24+Interaction runs the other way. A button carries an id the host chose; pressing
25+it calls `on_press` with that id, the host mutates its own state, and the next
26+`on_view` reflects it.
2027
2128 ```c
22-typedef void (*cosmic_on_press)(void *ctx, int32_t button_id,
23- char *out, size_t out_len);
29+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b);
30+typedef void (*cosmic_on_press)(void *ctx, int32_t id);
2431 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */
2532 ```
2633
34+Because the tree is rebuilt every frame, it can depend on state: a button
35+disappears, or goes inert, simply by not being described that way this time
36+round. See `cosmic_ffi.h` for the full list of containers, leaves and
37+attributes.
38+
2739 `cosmic_run` must be called from the main thread, and `ctx` is only ever
2840 touched from that thread.
2941
30-## Using a prebuilt release
42+## From Nim
43+
44+Containers are block templates, so the source has the same shape as the window:
45+
46+```nim
47+proc onView(ctx: pointer; b: Builder) {.cdecl.} =
48+ let c = cast[ptr Counter](ctx)
49+ b.container:
50+ b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
51+ b.text($c.value, TextTitle1)
52+ b.row:
53+ b.spacing(space(SpaceS))
54+ b.button("", IdDec)
55+ b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
56+ b.button("+", IdInc, ButtonSuggested)
57+```
58+
59+## Running it
60+
61+The demo needs `nim/libcosmic_ffi.so`. Take it from a release:
3162
3263 ```bash
33-tar xzf cosmic_ffi-<version>-x86_64-unknown-linux-gnu.tar.gz
34-cp cosmic_ffi-*/libcosmic_ffi.so nim/
35-cd nim && nim c -r app.nim
64+export RICKUB_TOKEN=... # the repo is private
65+just fetch <asset-url-from-the-release-page>
66+just run
3667 ```
3768
38-## Building from source
69+or build it yourself — the cold build is long, which is what the release CI is
70+for:
3971
4072 ```bash
41-cd cosmic_ffi && cargo build --release
42-cp target/release/libcosmic_ffi.so ../nim/
43-cd ../nim && nim c -r app.nim
73+just build
74+just run
4475 ```
76+
77+`just check` type-checks the Nim without needing the library at all, since the
78+binding loads it lazily.
@@ -5,40 +5,74 @@
5 libcosmic's API is generics, traits and closures, so it has no C ABI of its own —5 libcosmic's API is generics, traits and closures, so it has no C ABI of its own —
6 building libcosmic itself as a `cdylib` exports nothing. The `cosmic_ffi` crate6 building libcosmic itself as a `cdylib` exports nothing. The `cosmic_ffi` crate
7 therefore *defines* the C surface: it depends on libcosmic as a normal Rust7 therefore *defines* the C surface: it depends on libcosmic as a normal Rust
8-crate and exports one function.8+crate and exports a handful of functions.
9 9
10 ```10 ```
11 cosmic_ffi/ Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h11 cosmic_ffi/ Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h
12 nim/ Nim bindings (cosmic.nim) and the demo app (app.nim)12 nim/ Nim bindings (cosmic.nim) and the demo app (app.nim)
13+justfile build / run / fetch tasks
13 ```14 ```
14 15
15 ## The contract16 ## The contract
16 17
17-Rust owns the widget tree; the host owns the application state. Each button18+The host owns the application state *and* the shape of the window. Once per
18-press is handed back through `on_press`, which writes the next label into the19+frame libcosmic calls back through `on_view` with an opaque builder, and the
19-buffer it is given.20+host describes its widget tree by calling the builder functions. Widget memory
21+never crosses the boundary — the host only ever issues calls, and the builder
22+handle is dead the moment `on_view` returns.
23+
24+Interaction runs the other way. A button carries an id the host chose; pressing
25+it calls `on_press` with that id, the host mutates its own state, and the next
26+`on_view` reflects it.
20 27
21 ```c28 ```c
22-typedef void (*cosmic_on_press)(void *ctx, int32_t button_id,29+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b);
23- char *out, size_t out_len);30+typedef void (*cosmic_on_press)(void *ctx, int32_t id);
24 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */31 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */
25 ```32 ```
26 33
34+Because the tree is rebuilt every frame, it can depend on state: a button
35+disappears, or goes inert, simply by not being described that way this time
36+round. See `cosmic_ffi.h` for the full list of containers, leaves and
37+attributes.
38+
27 `cosmic_run` must be called from the main thread, and `ctx` is only ever39 `cosmic_run` must be called from the main thread, and `ctx` is only ever
28 touched from that thread.40 touched from that thread.
29 41
30-## Using a prebuilt release42+## From Nim
43+
44+Containers are block templates, so the source has the same shape as the window:
45+
46+```nim
47+proc onView(ctx: pointer; b: Builder) {.cdecl.} =
48+ let c = cast[ptr Counter](ctx)
49+ b.container:
50+ b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
51+ b.text($c.value, TextTitle1)
52+ b.row:
53+ b.spacing(space(SpaceS))
54+ b.button("", IdDec)
55+ b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
56+ b.button("+", IdInc, ButtonSuggested)
57+```
58+
59+## Running it
60+
61+The demo needs `nim/libcosmic_ffi.so`. Take it from a release:
31 62
32 ```bash63 ```bash
33-tar xzf cosmic_ffi-<version>-x86_64-unknown-linux-gnu.tar.gz64+export RICKUB_TOKEN=... # the repo is private
34-cp cosmic_ffi-*/libcosmic_ffi.so nim/65+just fetch <asset-url-from-the-release-page>
35-cd nim && nim c -r app.nim66+just run
36 ```67 ```
37 68
38-## Building from source69+or build it yourself — the cold build is long, which is what the release CI is
70+for:
39 71
40 ```bash72 ```bash
41-cd cosmic_ffi && cargo build --release73+just build
42-cp target/release/libcosmic_ffi.so ../nim/74+just run
43-cd ../nim && nim c -r app.nim
44 ```75 ```
76+
77+`just check` type-checks the Nim without needing the library at all, since the
78+binding loads it lazily.
modified cosmic_ffi/Cargo.lock +1 -1
@@ -964,7 +964,7 @@ dependencies = [
964964
965965 [[package]]
966966 name = "cosmic_ffi"
967-version = "0.1.0"
967+version = "0.2.0"
968968 dependencies = [
969969 "libcosmic",
970970 ]
@@ -964,7 +964,7 @@ dependencies = [
964 964
965 [[package]]965 [[package]]
966 name = "cosmic_ffi"966 name = "cosmic_ffi"
967-version = "0.1.0"967+version = "0.2.0"
968 dependencies = [968 dependencies = [
969 "libcosmic",969 "libcosmic",
970 ]970 ]
modified cosmic_ffi/Cargo.toml +1 -1
@@ -1,6 +1,6 @@
11 [package]
22 name = "cosmic_ffi"
3-version = "0.1.0"
3+version = "0.2.0"
44 edition = "2021"
55
66 [lib]
@@ -1,6 +1,6 @@
1 [package]1 [package]
2 name = "cosmic_ffi"2 name = "cosmic_ffi"
3-version = "0.1.0"3+version = "0.2.0"
4 edition = "2021"4 edition = "2021"
5 5
6 [lib]6 [lib]
modified cosmic_ffi/cosmic_ffi.h +61 -9
@@ -1,25 +1,77 @@
1-/* C ABI over libcosmic. See src/lib.rs for the contract. */
1+/* C ABI over libcosmic. See src/lib.rs for the contract.
2+
3+ The host owns the state and the shape of the window. Each frame libcosmic
4+ calls `on_view`, and the host describes its widget tree by calling the
5+ builder functions below on the handle it is given. Presses come back
6+ through `on_press` carrying the id the host put on the button. */
27 #ifndef COSMIC_FFI_H
38 #define COSMIC_FFI_H
49
510 #include <stddef.h>
611 #include <stdint.h>
712
8-/* Fills `out` (a buffer of `out_len` bytes) with the NUL-terminated text to
9- display after button `button_id` was pressed. 0 = left, 1 = right. */
10-typedef void (*cosmic_on_press)(void *ctx, int32_t button_id, char *out,
11- size_t out_len);
13+/* Opaque. Valid only for the duration of the on_view call it arrived with. */
14+typedef struct CosmicBuilder CosmicBuilder;
15+
16+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder);
17+typedef void (*cosmic_on_press)(void *ctx, int32_t id);
1218
1319 typedef struct {
1420 const char *title;
15- const char *initial_text;
16- const char *left_label;
17- const char *right_label;
21+ cosmic_on_view on_view;
1822 cosmic_on_press on_press;
1923 void *ctx;
24+ uint32_t width; /* 0 for a default */
25+ uint32_t height; /* 0 for a default */
2026 } CosmicConfig;
2127
22-/* Opens the window and blocks until it closes. 0 on success. */
28+/* Opens the window and blocks until it closes. 0 on success.
29+ Must be called from the main thread. */
2330 int32_t cosmic_run(const CosmicConfig *config);
2431
32+/* --- containers: each open must be matched by a cosmic_end --- */
33+void cosmic_column(CosmicBuilder *b);
34+void cosmic_row(CosmicBuilder *b);
35+void cosmic_container(CosmicBuilder *b);
36+void cosmic_end(CosmicBuilder *b);
37+
38+/* --- attributes of the innermost open container --- */
39+void cosmic_spacing(CosmicBuilder *b, float px);
40+void cosmic_padding(CosmicBuilder *b, float px);
41+void cosmic_align_center(CosmicBuilder *b);
42+void cosmic_fill(CosmicBuilder *b);
43+
44+/* --- leaves --- */
45+#define COSMIC_TEXT_BODY 0
46+#define COSMIC_TEXT_TITLE1 1
47+#define COSMIC_TEXT_TITLE2 2
48+#define COSMIC_TEXT_TITLE3 3
49+#define COSMIC_TEXT_TITLE4 4
50+#define COSMIC_TEXT_HEADING 5
51+#define COSMIC_TEXT_CAPTION 6
52+#define COSMIC_TEXT_MONOTEXT 7
53+void cosmic_text(CosmicBuilder *b, int32_t style, const char *text);
54+
55+#define COSMIC_BUTTON_STANDARD 0
56+#define COSMIC_BUTTON_SUGGESTED 1
57+#define COSMIC_BUTTON_DESTRUCTIVE 2
58+#define COSMIC_BUTTON_TEXT 3
59+#define COSMIC_BUTTON_LINK 4
60+/* A negative id makes the button inert, i.e. disabled. */
61+void cosmic_button(CosmicBuilder *b, int32_t style, const char *label,
62+ int32_t id);
63+
64+void cosmic_space(CosmicBuilder *b, float w, float h);
65+
66+/* --- the active theme's spacing scale, for laying out in COSMIC's rhythm --- */
67+#define COSMIC_SPACE_NONE 0
68+#define COSMIC_SPACE_XXXS 1
69+#define COSMIC_SPACE_XXS 2
70+#define COSMIC_SPACE_XS 3
71+#define COSMIC_SPACE_S 4
72+#define COSMIC_SPACE_M 5
73+#define COSMIC_SPACE_L 6
74+#define COSMIC_SPACE_XL 7
75+float cosmic_space_unit(int32_t step);
76+
2577 #endif
@@ -1,25 +1,77 @@
1-/* C ABI over libcosmic. See src/lib.rs for the contract. */1+/* C ABI over libcosmic. See src/lib.rs for the contract.
2+
3+ The host owns the state and the shape of the window. Each frame libcosmic
4+ calls `on_view`, and the host describes its widget tree by calling the
5+ builder functions below on the handle it is given. Presses come back
6+ through `on_press` carrying the id the host put on the button. */
2 #ifndef COSMIC_FFI_H7 #ifndef COSMIC_FFI_H
3 #define COSMIC_FFI_H8 #define COSMIC_FFI_H
4 9
5 #include <stddef.h>10 #include <stddef.h>
6 #include <stdint.h>11 #include <stdint.h>
7 12
8-/* Fills `out` (a buffer of `out_len` bytes) with the NUL-terminated text to13+/* Opaque. Valid only for the duration of the on_view call it arrived with. */
9- display after button `button_id` was pressed. 0 = left, 1 = right. */14+typedef struct CosmicBuilder CosmicBuilder;
10-typedef void (*cosmic_on_press)(void *ctx, int32_t button_id, char *out,15+
11- size_t out_len);16+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder);
17+typedef void (*cosmic_on_press)(void *ctx, int32_t id);
12 18
13 typedef struct {19 typedef struct {
14 const char *title;20 const char *title;
15- const char *initial_text;21+ cosmic_on_view on_view;
16- const char *left_label;
17- const char *right_label;
18 cosmic_on_press on_press;22 cosmic_on_press on_press;
19 void *ctx;23 void *ctx;
24+ uint32_t width; /* 0 for a default */
25+ uint32_t height; /* 0 for a default */
20 } CosmicConfig;26 } CosmicConfig;
21 27
22-/* Opens the window and blocks until it closes. 0 on success. */28+/* Opens the window and blocks until it closes. 0 on success.
29+ Must be called from the main thread. */
23 int32_t cosmic_run(const CosmicConfig *config);30 int32_t cosmic_run(const CosmicConfig *config);
24 31
32+/* --- containers: each open must be matched by a cosmic_end --- */
33+void cosmic_column(CosmicBuilder *b);
34+void cosmic_row(CosmicBuilder *b);
35+void cosmic_container(CosmicBuilder *b);
36+void cosmic_end(CosmicBuilder *b);
37+
38+/* --- attributes of the innermost open container --- */
39+void cosmic_spacing(CosmicBuilder *b, float px);
40+void cosmic_padding(CosmicBuilder *b, float px);
41+void cosmic_align_center(CosmicBuilder *b);
42+void cosmic_fill(CosmicBuilder *b);
43+
44+/* --- leaves --- */
45+#define COSMIC_TEXT_BODY 0
46+#define COSMIC_TEXT_TITLE1 1
47+#define COSMIC_TEXT_TITLE2 2
48+#define COSMIC_TEXT_TITLE3 3
49+#define COSMIC_TEXT_TITLE4 4
50+#define COSMIC_TEXT_HEADING 5
51+#define COSMIC_TEXT_CAPTION 6
52+#define COSMIC_TEXT_MONOTEXT 7
53+void cosmic_text(CosmicBuilder *b, int32_t style, const char *text);
54+
55+#define COSMIC_BUTTON_STANDARD 0
56+#define COSMIC_BUTTON_SUGGESTED 1
57+#define COSMIC_BUTTON_DESTRUCTIVE 2
58+#define COSMIC_BUTTON_TEXT 3
59+#define COSMIC_BUTTON_LINK 4
60+/* A negative id makes the button inert, i.e. disabled. */
61+void cosmic_button(CosmicBuilder *b, int32_t style, const char *label,
62+ int32_t id);
63+
64+void cosmic_space(CosmicBuilder *b, float w, float h);
65+
66+/* --- the active theme's spacing scale, for laying out in COSMIC's rhythm --- */
67+#define COSMIC_SPACE_NONE 0
68+#define COSMIC_SPACE_XXXS 1
69+#define COSMIC_SPACE_XXS 2
70+#define COSMIC_SPACE_XS 3
71+#define COSMIC_SPACE_S 4
72+#define COSMIC_SPACE_M 5
73+#define COSMIC_SPACE_L 6
74+#define COSMIC_SPACE_XL 7
75+float cosmic_space_unit(int32_t step);
76+
25 #endif77 #endif
modified cosmic_ffi/src/lib.rs +299 -66
@@ -1,30 +1,292 @@
11 //! A tiny C ABI over libcosmic so non-Rust hosts can drive a COSMIC window.
22 //!
3-//! The Rust side owns the widget tree; the host owns the application state.
4-//! Every button press is handed back to the host through `on_press`, which
5-//! writes the new label text into the buffer it is given.
3+//! The host owns both the application state *and* the shape of the window.
4+//! Each frame, libcosmic calls back into the host through `on_view` with a
5+//! [`Builder`]; the host describes its widget tree by calling the `cosmic_*`
6+//! builder functions, and Rust turns that into real libcosmic widgets. Widget
7+//! memory never crosses the boundary — the host only ever issues calls.
8+//!
9+//! Interaction runs the other way: a button carries an id the host chose, and
10+//! pressing it calls `on_press` with that id. The host mutates its own state
11+//! and the next `on_view` reflects it. That is the whole loop.
612
713 use std::ffi::{c_char, c_void, CStr};
814
9-/// Called when a button is pressed. `out` is a NUL-terminated buffer of
10-/// `out_len` bytes the host fills with the text to display next.
11-pub type OnPress =
12- Option<unsafe extern "C" fn(ctx: *mut c_void, button_id: i32, out: *mut c_char, out_len: usize)>;
15+use cosmic::iced::{Alignment, Length};
16+use cosmic::widget;
17+
18+/// Describes the widget tree for one frame. Only valid for the duration of
19+/// the `on_view` call it was handed to.
20+pub type OnView = Option<unsafe extern "C" fn(ctx: *mut c_void, builder: *mut Builder)>;
21+
22+/// Called when the button carrying `id` is pressed.
23+pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>;
1324
1425 #[repr(C)]
1526 pub struct CosmicConfig {
1627 pub title: *const c_char,
17- pub initial_text: *const c_char,
18- pub left_label: *const c_char,
19- pub right_label: *const c_char,
28+ pub on_view: OnView,
2029 pub on_press: OnPress,
2130 pub ctx: *mut c_void,
31+ /// Initial window size. 0 means "pick a default".
32+ pub width: u32,
33+ pub height: u32,
34+}
35+
36+type Element = cosmic::Element<'static, Message>;
37+
38+#[derive(Debug, Clone, Copy)]
39+enum Message {
40+ Pressed(i32),
41+}
42+
43+// ---------------------------------------------------------------- builder
44+
45+#[derive(Clone, Copy, PartialEq)]
46+enum Kind {
47+ Column,
48+ Row,
49+ Container,
50+}
51+
52+/// One open container and the children accumulated into it so far.
53+struct Frame {
54+ kind: Kind,
55+ children: Vec<Element>,
56+ spacing: f32,
57+ padding: f32,
58+ align_center: bool,
59+ fill: bool,
60+}
61+
62+impl Frame {
63+ fn new(kind: Kind) -> Self {
64+ Frame {
65+ kind,
66+ children: Vec::new(),
67+ spacing: 0.0,
68+ padding: 0.0,
69+ align_center: false,
70+ fill: false,
71+ }
72+ }
73+
74+ fn build(self) -> Element {
75+ match self.kind {
76+ Kind::Column => {
77+ let mut c = widget::column::with_children(self.children)
78+ .spacing(self.spacing)
79+ .padding(self.padding);
80+ if self.align_center {
81+ c = c.align_x(Alignment::Center);
82+ }
83+ if self.fill {
84+ c = c.width(Length::Fill).height(Length::Fill);
85+ }
86+ c.into()
87+ }
88+ Kind::Row => {
89+ let mut r = widget::row::with_children(self.children)
90+ .spacing(self.spacing)
91+ .padding(self.padding);
92+ if self.align_center {
93+ r = r.align_y(Alignment::Center);
94+ }
95+ if self.fill {
96+ r = r.width(Length::Fill);
97+ }
98+ r.into()
99+ }
100+ Kind::Container => {
101+ // A container holds one child, so stack its children in a column.
102+ let mut inner = widget::column::with_children(self.children).spacing(self.spacing);
103+ if self.align_center {
104+ inner = inner.align_x(Alignment::Center);
105+ }
106+ let mut ct = widget::container(inner).padding(self.padding);
107+ ct = if self.fill {
108+ ct.center(Length::Fill)
109+ } else if self.align_center {
110+ ct.center_x(Length::Fill)
111+ } else {
112+ ct
113+ };
114+ ct.into()
115+ }
116+ }
117+ }
118+}
119+
120+/// The opaque handle the host builds through. Never outlives one `on_view`.
121+pub struct Builder {
122+ /// Always non-empty: `stack[0]` is the implicit root.
123+ stack: Vec<Frame>,
124+}
125+
126+impl Builder {
127+ fn new() -> Self {
128+ Builder {
129+ stack: vec![Frame::new(Kind::Container)],
130+ }
131+ }
132+
133+ fn top(&mut self) -> &mut Frame {
134+ // The root is never popped, so this cannot fail.
135+ self.stack.last_mut().expect("root frame")
136+ }
137+
138+ fn push(&mut self, kind: Kind) {
139+ self.stack.push(Frame::new(kind));
140+ }
141+
142+ fn pop(&mut self) {
143+ // Ignore an unbalanced `cosmic_end`: closing the root is a host bug,
144+ // but tearing down the window over it helps nobody.
145+ if self.stack.len() > 1 {
146+ let done = self.stack.pop().expect("checked len").build();
147+ self.top().children.push(done);
148+ }
149+ }
150+
151+ fn leaf(&mut self, element: Element) {
152+ self.top().children.push(element);
153+ }
154+
155+ /// Close anything the host left open and produce the frame's tree.
156+ fn finish(mut self) -> Element {
157+ while self.stack.len() > 1 {
158+ self.pop();
159+ }
160+ self.stack.pop().expect("root frame").build()
161+ }
22162 }
23163
24-const OUT_LEN: usize = 256;
164+/// Borrow a builder pointer, doing nothing if the host passed NULL.
165+macro_rules! builder {
166+ ($ptr:expr) => {
167+ match $ptr.as_mut() {
168+ Some(b) => b,
169+ None => return,
170+ }
171+ };
172+}
173+
174+macro_rules! builder_fn {
175+ ($(#[$m:meta])* $name:ident ($b:ident $(, $arg:ident : $ty:ty)*) $body:block) => {
176+ $(#[$m])*
177+ #[no_mangle]
178+ pub unsafe extern "C" fn $name(builder: *mut Builder $(, $arg: $ty)*) {
179+ let $b = builder!(builder);
180+ $body
181+ }
182+ };
183+}
184+
185+builder_fn!(/// Open a vertical container. Close it with `cosmic_end`.
186+ cosmic_column(b) { b.push(Kind::Column) });
187+builder_fn!(/// Open a horizontal container. Close it with `cosmic_end`.
188+ cosmic_row(b) { b.push(Kind::Row) });
189+builder_fn!(/// Open a plain container. Close it with `cosmic_end`.
190+ cosmic_container(b) { b.push(Kind::Container) });
191+builder_fn!(/// Close the innermost open container.
192+ cosmic_end(b) { b.pop() });
193+
194+builder_fn!(/// Gap between the open container's children, in pixels.
195+ cosmic_spacing(b, px: f32) { b.top().spacing = px });
196+builder_fn!(/// Inset around the open container's children, in pixels.
197+ cosmic_padding(b, px: f32) { b.top().padding = px });
198+builder_fn!(/// Centre the open container's children across its main axis.
199+ cosmic_align_center(b) { b.top().align_center = true });
200+builder_fn!(/// Let the open container take all the space it is offered.
201+ cosmic_fill(b) { b.top().fill = true });
202+
203+builder_fn!(/// A blank gap of `w` by `h` pixels.
204+ cosmic_space(b, w: f32, h: f32) {
205+ b.leaf(widget::Space::new(Length::Fixed(w), Length::Fixed(h)).into())
206+ });
207+
208+/// Text styles for [`cosmic_text`], matching COSMIC's type scale.
209+pub const TEXT_BODY: i32 = 0;
210+pub const TEXT_TITLE1: i32 = 1;
211+pub const TEXT_TITLE2: i32 = 2;
212+pub const TEXT_TITLE3: i32 = 3;
213+pub const TEXT_TITLE4: i32 = 4;
214+pub const TEXT_HEADING: i32 = 5;
215+pub const TEXT_CAPTION: i32 = 6;
216+pub const TEXT_MONOTEXT: i32 = 7;
217+
218+builder_fn!(/// A run of text in one of the `COSMIC_TEXT_*` styles.
219+ cosmic_text(b, style: i32, text: *const c_char) {
220+ let s = str_or(text, "");
221+ b.leaf(match style {
222+ TEXT_TITLE1 => widget::text::title1(s).into(),
223+ TEXT_TITLE2 => widget::text::title2(s).into(),
224+ TEXT_TITLE3 => widget::text::title3(s).into(),
225+ TEXT_TITLE4 => widget::text::title4(s).into(),
226+ TEXT_HEADING => widget::text::heading(s).into(),
227+ TEXT_CAPTION => widget::text::caption(s).into(),
228+ TEXT_MONOTEXT => widget::text::monotext(s).into(),
229+ _ => widget::text::body(s).into(),
230+ })
231+ });
232+
233+/// Button styles for [`cosmic_button`].
234+pub const BUTTON_STANDARD: i32 = 0;
235+pub const BUTTON_SUGGESTED: i32 = 1;
236+pub const BUTTON_DESTRUCTIVE: i32 = 2;
237+pub const BUTTON_TEXT: i32 = 3;
238+pub const BUTTON_LINK: i32 = 4;
239+
240+builder_fn!(/// A button that reports `id` to `on_press`. A negative `id`
241+ /// makes it inert, which is how a disabled button is expressed.
242+ cosmic_button(b, style: i32, label: *const c_char, id: i32) {
243+ let s = str_or(label, "");
244+ let button = match style {
245+ BUTTON_SUGGESTED => widget::button::suggested(s),
246+ BUTTON_DESTRUCTIVE => widget::button::destructive(s),
247+ BUTTON_TEXT => widget::button::text(s),
248+ BUTTON_LINK => widget::button::link(s),
249+ _ => widget::button::standard(s),
250+ };
251+ b.leaf(if id < 0 {
252+ button.into()
253+ } else {
254+ button.on_press(Message::Pressed(id)).into()
255+ })
256+ });
257+
258+/// Spacing steps for [`cosmic_space_unit`].
259+pub const SPACE_NONE: i32 = 0;
260+pub const SPACE_XXXS: i32 = 1;
261+pub const SPACE_XXS: i32 = 2;
262+pub const SPACE_XS: i32 = 3;
263+pub const SPACE_S: i32 = 4;
264+pub const SPACE_M: i32 = 5;
265+pub const SPACE_L: i32 = 6;
266+pub const SPACE_XL: i32 = 7;
25267
26-/// The host's callback plus its opaque context, moved onto the UI thread.
268+/// The active theme's spacing for `step`, so hosts can lay out in COSMIC's
269+/// rhythm instead of hardcoding pixels.
270+#[no_mangle]
271+pub extern "C" fn cosmic_space_unit(step: i32) -> f32 {
272+ let s = cosmic::theme::active().cosmic().spacing;
273+ f32::from(match step {
274+ SPACE_XXXS => s.space_xxxs,
275+ SPACE_XXS => s.space_xxs,
276+ SPACE_XS => s.space_xs,
277+ SPACE_S => s.space_s,
278+ SPACE_M => s.space_m,
279+ SPACE_L => s.space_l,
280+ SPACE_XL => s.space_xl,
281+ _ => 0,
282+ })
283+}
284+
285+// -------------------------------------------------------------------- app
286+
287+/// The host's callbacks plus its opaque context, moved onto the UI thread.
27288 struct Host {
289+ on_view: OnView,
28290 on_press: OnPress,
29291 ctx: *mut c_void,
30292 }
@@ -34,28 +296,26 @@ struct Host {
34296 unsafe impl Send for Host {}
35297
36298 impl Host {
37- fn press(&self, button_id: i32) -> Option<String> {
38- let cb = self.on_press?;
39- let mut buf = [0u8; OUT_LEN];
40- unsafe { cb(self.ctx, button_id, buf.as_mut_ptr().cast(), OUT_LEN) };
41- let end = buf.iter().position(|&b| b == 0).unwrap_or(OUT_LEN);
42- Some(String::from_utf8_lossy(&buf[..end]).into_owned())
299+ fn view(&self) -> Element {
300+ let mut builder = Builder::new();
301+ if let Some(cb) = self.on_view {
302+ unsafe { cb(self.ctx, &mut builder) };
303+ }
304+ builder.finish()
305+ }
306+
307+ fn press(&self, id: i32) {
308+ if let Some(cb) = self.on_press {
309+ unsafe { cb(self.ctx, id) };
310+ }
43311 }
44312 }
45313
46314 struct Flags {
47315 title: String,
48- text: String,
49- left: String,
50- right: String,
51316 host: Host,
52317 }
53318
54-#[derive(Debug, Clone, Copy)]
55-enum Message {
56- Pressed(i32),
57-}
58-
59319 struct App {
60320 core: cosmic::app::Core,
61321 flags: Flags,
@@ -81,45 +341,18 @@ impl cosmic::Application for App {
81341 }
82342
83343 fn header_start(&self) -> Vec<cosmic::Element<Message>> {
84- vec![cosmic::widget::text::heading(self.flags.title.clone()).into()]
344+ vec![widget::text::heading(self.flags.title.clone()).into()]
85345 }
86346
87347 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {
88348 match message {
89- Message::Pressed(id) => {
90- if let Some(text) = self.flags.host.press(id) {
91- self.flags.text = text;
92- }
93- }
349+ Message::Pressed(id) => self.flags.host.press(id),
94350 }
95351 cosmic::app::Task::none()
96352 }
97353
98354 fn view(&self) -> cosmic::Element<Message> {
99- use cosmic::widget;
100-
101- let spacing = cosmic::theme::active().cosmic().spacing;
102-
103- let buttons = widget::row::with_children(vec![
104- widget::button::standard(self.flags.left.clone())
105- .on_press(Message::Pressed(0))
106- .into(),
107- widget::button::suggested(self.flags.right.clone())
108- .on_press(Message::Pressed(1))
109- .into(),
110- ])
111- .spacing(spacing.space_s);
112-
113- widget::container(
114- widget::column::with_children(vec![
115- widget::text::title1(self.flags.text.clone()).into(),
116- buttons.into(),
117- ])
118- .spacing(spacing.space_m)
119- .align_x(cosmic::iced::Alignment::Center),
120- )
121- .center(cosmic::iced::Length::Fill)
122- .into()
355+ self.flags.host.view()
123356 }
124357 }
125358
@@ -135,28 +368,28 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String {
135368 ///
136369 /// # Safety
137370 /// `config` must point to a valid `CosmicConfig` whose strings are
138-/// NUL-terminated and live until this call returns.
371+/// NUL-terminated and live until this call returns. Must be called from the
372+/// main thread.
139373 #[no_mangle]
140374 pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 {
141- if config.is_null() {
375+ let Some(c) = config.as_ref() else {
142376 return -1;
143- }
144- let c = &*config;
377+ };
145378
146379 let flags = Flags {
147380 title: str_or(c.title, "Cosmic"),
148- text: str_or(c.initial_text, ""),
149- left: str_or(c.left_label, "-"),
150- right: str_or(c.right_label, "+"),
151381 host: Host {
382+ on_view: c.on_view,
152383 on_press: c.on_press,
153384 ctx: c.ctx,
154385 },
155386 };
156387
157- let settings = cosmic::app::Settings::default()
158- .size(cosmic::iced::Size::new(420.0, 260.0))
159- .debug(false);
388+ let size = cosmic::iced::Size::new(
389+ if c.width == 0 { 420.0 } else { c.width as f32 },
390+ if c.height == 0 { 260.0 } else { c.height as f32 },
391+ );
392+ let settings = cosmic::app::Settings::default().size(size).debug(false);
160393
161394 match cosmic::app::run::<App>(settings, flags) {
162395 Ok(()) => 0,
@@ -1,30 +1,292 @@
1 //! A tiny C ABI over libcosmic so non-Rust hosts can drive a COSMIC window.1 //! A tiny C ABI over libcosmic so non-Rust hosts can drive a COSMIC window.
2 //!2 //!
3-//! The Rust side owns the widget tree; the host owns the application state.3+//! The host owns both the application state *and* the shape of the window.
4-//! Every button press is handed back to the host through `on_press`, which4+//! Each frame, libcosmic calls back into the host through `on_view` with a
5-//! writes the new label text into the buffer it is given.5+//! [`Builder`]; the host describes its widget tree by calling the `cosmic_*`
6+//! builder functions, and Rust turns that into real libcosmic widgets. Widget
7+//! memory never crosses the boundary — the host only ever issues calls.
8+//!
9+//! Interaction runs the other way: a button carries an id the host chose, and
10+//! pressing it calls `on_press` with that id. The host mutates its own state
11+//! and the next `on_view` reflects it. That is the whole loop.
6 12
7 use std::ffi::{c_char, c_void, CStr};13 use std::ffi::{c_char, c_void, CStr};
8 14
9-/// Called when a button is pressed. `out` is a NUL-terminated buffer of15+use cosmic::iced::{Alignment, Length};
10-/// `out_len` bytes the host fills with the text to display next.16+use cosmic::widget;
11-pub type OnPress =17+
12- Option<unsafe extern "C" fn(ctx: *mut c_void, button_id: i32, out: *mut c_char, out_len: usize)>;18+/// Describes the widget tree for one frame. Only valid for the duration of
19+/// the `on_view` call it was handed to.
20+pub type OnView = Option<unsafe extern "C" fn(ctx: *mut c_void, builder: *mut Builder)>;
21+
22+/// Called when the button carrying `id` is pressed.
23+pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>;
13 24
14 #[repr(C)]25 #[repr(C)]
15 pub struct CosmicConfig {26 pub struct CosmicConfig {
16 pub title: *const c_char,27 pub title: *const c_char,
17- pub initial_text: *const c_char,28+ pub on_view: OnView,
18- pub left_label: *const c_char,
19- pub right_label: *const c_char,
20 pub on_press: OnPress,29 pub on_press: OnPress,
21 pub ctx: *mut c_void,30 pub ctx: *mut c_void,
31+ /// Initial window size. 0 means "pick a default".
32+ pub width: u32,
33+ pub height: u32,
34+}
35+
36+type Element = cosmic::Element<'static, Message>;
37+
38+#[derive(Debug, Clone, Copy)]
39+enum Message {
40+ Pressed(i32),
41+}
42+
43+// ---------------------------------------------------------------- builder
44+
45+#[derive(Clone, Copy, PartialEq)]
46+enum Kind {
47+ Column,
48+ Row,
49+ Container,
50+}
51+
52+/// One open container and the children accumulated into it so far.
53+struct Frame {
54+ kind: Kind,
55+ children: Vec<Element>,
56+ spacing: f32,
57+ padding: f32,
58+ align_center: bool,
59+ fill: bool,
60+}
61+
62+impl Frame {
63+ fn new(kind: Kind) -> Self {
64+ Frame {
65+ kind,
66+ children: Vec::new(),
67+ spacing: 0.0,
68+ padding: 0.0,
69+ align_center: false,
70+ fill: false,
71+ }
72+ }
73+
74+ fn build(self) -> Element {
75+ match self.kind {
76+ Kind::Column => {
77+ let mut c = widget::column::with_children(self.children)
78+ .spacing(self.spacing)
79+ .padding(self.padding);
80+ if self.align_center {
81+ c = c.align_x(Alignment::Center);
82+ }
83+ if self.fill {
84+ c = c.width(Length::Fill).height(Length::Fill);
85+ }
86+ c.into()
87+ }
88+ Kind::Row => {
89+ let mut r = widget::row::with_children(self.children)
90+ .spacing(self.spacing)
91+ .padding(self.padding);
92+ if self.align_center {
93+ r = r.align_y(Alignment::Center);
94+ }
95+ if self.fill {
96+ r = r.width(Length::Fill);
97+ }
98+ r.into()
99+ }
100+ Kind::Container => {
101+ // A container holds one child, so stack its children in a column.
102+ let mut inner = widget::column::with_children(self.children).spacing(self.spacing);
103+ if self.align_center {
104+ inner = inner.align_x(Alignment::Center);
105+ }
106+ let mut ct = widget::container(inner).padding(self.padding);
107+ ct = if self.fill {
108+ ct.center(Length::Fill)
109+ } else if self.align_center {
110+ ct.center_x(Length::Fill)
111+ } else {
112+ ct
113+ };
114+ ct.into()
115+ }
116+ }
117+ }
118+}
119+
120+/// The opaque handle the host builds through. Never outlives one `on_view`.
121+pub struct Builder {
122+ /// Always non-empty: `stack[0]` is the implicit root.
123+ stack: Vec<Frame>,
124+}
125+
126+impl Builder {
127+ fn new() -> Self {
128+ Builder {
129+ stack: vec![Frame::new(Kind::Container)],
130+ }
131+ }
132+
133+ fn top(&mut self) -> &mut Frame {
134+ // The root is never popped, so this cannot fail.
135+ self.stack.last_mut().expect("root frame")
136+ }
137+
138+ fn push(&mut self, kind: Kind) {
139+ self.stack.push(Frame::new(kind));
140+ }
141+
142+ fn pop(&mut self) {
143+ // Ignore an unbalanced `cosmic_end`: closing the root is a host bug,
144+ // but tearing down the window over it helps nobody.
145+ if self.stack.len() > 1 {
146+ let done = self.stack.pop().expect("checked len").build();
147+ self.top().children.push(done);
148+ }
149+ }
150+
151+ fn leaf(&mut self, element: Element) {
152+ self.top().children.push(element);
153+ }
154+
155+ /// Close anything the host left open and produce the frame's tree.
156+ fn finish(mut self) -> Element {
157+ while self.stack.len() > 1 {
158+ self.pop();
159+ }
160+ self.stack.pop().expect("root frame").build()
161+ }
22 }162 }
23 163
24-const OUT_LEN: usize = 256;164+/// Borrow a builder pointer, doing nothing if the host passed NULL.
165+macro_rules! builder {
166+ ($ptr:expr) => {
167+ match $ptr.as_mut() {
168+ Some(b) => b,
169+ None => return,
170+ }
171+ };
172+}
173+
174+macro_rules! builder_fn {
175+ ($(#[$m:meta])* $name:ident ($b:ident $(, $arg:ident : $ty:ty)*) $body:block) => {
176+ $(#[$m])*
177+ #[no_mangle]
178+ pub unsafe extern "C" fn $name(builder: *mut Builder $(, $arg: $ty)*) {
179+ let $b = builder!(builder);
180+ $body
181+ }
182+ };
183+}
184+
185+builder_fn!(/// Open a vertical container. Close it with `cosmic_end`.
186+ cosmic_column(b) { b.push(Kind::Column) });
187+builder_fn!(/// Open a horizontal container. Close it with `cosmic_end`.
188+ cosmic_row(b) { b.push(Kind::Row) });
189+builder_fn!(/// Open a plain container. Close it with `cosmic_end`.
190+ cosmic_container(b) { b.push(Kind::Container) });
191+builder_fn!(/// Close the innermost open container.
192+ cosmic_end(b) { b.pop() });
193+
194+builder_fn!(/// Gap between the open container's children, in pixels.
195+ cosmic_spacing(b, px: f32) { b.top().spacing = px });
196+builder_fn!(/// Inset around the open container's children, in pixels.
197+ cosmic_padding(b, px: f32) { b.top().padding = px });
198+builder_fn!(/// Centre the open container's children across its main axis.
199+ cosmic_align_center(b) { b.top().align_center = true });
200+builder_fn!(/// Let the open container take all the space it is offered.
201+ cosmic_fill(b) { b.top().fill = true });
202+
203+builder_fn!(/// A blank gap of `w` by `h` pixels.
204+ cosmic_space(b, w: f32, h: f32) {
205+ b.leaf(widget::Space::new(Length::Fixed(w), Length::Fixed(h)).into())
206+ });
207+
208+/// Text styles for [`cosmic_text`], matching COSMIC's type scale.
209+pub const TEXT_BODY: i32 = 0;
210+pub const TEXT_TITLE1: i32 = 1;
211+pub const TEXT_TITLE2: i32 = 2;
212+pub const TEXT_TITLE3: i32 = 3;
213+pub const TEXT_TITLE4: i32 = 4;
214+pub const TEXT_HEADING: i32 = 5;
215+pub const TEXT_CAPTION: i32 = 6;
216+pub const TEXT_MONOTEXT: i32 = 7;
217+
218+builder_fn!(/// A run of text in one of the `COSMIC_TEXT_*` styles.
219+ cosmic_text(b, style: i32, text: *const c_char) {
220+ let s = str_or(text, "");
221+ b.leaf(match style {
222+ TEXT_TITLE1 => widget::text::title1(s).into(),
223+ TEXT_TITLE2 => widget::text::title2(s).into(),
224+ TEXT_TITLE3 => widget::text::title3(s).into(),
225+ TEXT_TITLE4 => widget::text::title4(s).into(),
226+ TEXT_HEADING => widget::text::heading(s).into(),
227+ TEXT_CAPTION => widget::text::caption(s).into(),
228+ TEXT_MONOTEXT => widget::text::monotext(s).into(),
229+ _ => widget::text::body(s).into(),
230+ })
231+ });
232+
233+/// Button styles for [`cosmic_button`].
234+pub const BUTTON_STANDARD: i32 = 0;
235+pub const BUTTON_SUGGESTED: i32 = 1;
236+pub const BUTTON_DESTRUCTIVE: i32 = 2;
237+pub const BUTTON_TEXT: i32 = 3;
238+pub const BUTTON_LINK: i32 = 4;
239+
240+builder_fn!(/// A button that reports `id` to `on_press`. A negative `id`
241+ /// makes it inert, which is how a disabled button is expressed.
242+ cosmic_button(b, style: i32, label: *const c_char, id: i32) {
243+ let s = str_or(label, "");
244+ let button = match style {
245+ BUTTON_SUGGESTED => widget::button::suggested(s),
246+ BUTTON_DESTRUCTIVE => widget::button::destructive(s),
247+ BUTTON_TEXT => widget::button::text(s),
248+ BUTTON_LINK => widget::button::link(s),
249+ _ => widget::button::standard(s),
250+ };
251+ b.leaf(if id < 0 {
252+ button.into()
253+ } else {
254+ button.on_press(Message::Pressed(id)).into()
255+ })
256+ });
257+
258+/// Spacing steps for [`cosmic_space_unit`].
259+pub const SPACE_NONE: i32 = 0;
260+pub const SPACE_XXXS: i32 = 1;
261+pub const SPACE_XXS: i32 = 2;
262+pub const SPACE_XS: i32 = 3;
263+pub const SPACE_S: i32 = 4;
264+pub const SPACE_M: i32 = 5;
265+pub const SPACE_L: i32 = 6;
266+pub const SPACE_XL: i32 = 7;
25 267
26-/// The host's callback plus its opaque context, moved onto the UI thread.268+/// The active theme's spacing for `step`, so hosts can lay out in COSMIC's
269+/// rhythm instead of hardcoding pixels.
270+#[no_mangle]
271+pub extern "C" fn cosmic_space_unit(step: i32) -> f32 {
272+ let s = cosmic::theme::active().cosmic().spacing;
273+ f32::from(match step {
274+ SPACE_XXXS => s.space_xxxs,
275+ SPACE_XXS => s.space_xxs,
276+ SPACE_XS => s.space_xs,
277+ SPACE_S => s.space_s,
278+ SPACE_M => s.space_m,
279+ SPACE_L => s.space_l,
280+ SPACE_XL => s.space_xl,
281+ _ => 0,
282+ })
283+}
284+
285+// -------------------------------------------------------------------- app
286+
287+/// The host's callbacks plus its opaque context, moved onto the UI thread.
27 struct Host {288 struct Host {
289+ on_view: OnView,
28 on_press: OnPress,290 on_press: OnPress,
29 ctx: *mut c_void,291 ctx: *mut c_void,
30 }292 }
@@ -34,28 +296,26 @@ struct Host {
34 unsafe impl Send for Host {}296 unsafe impl Send for Host {}
35 297
36 impl Host {298 impl Host {
37- fn press(&self, button_id: i32) -> Option<String> {299+ fn view(&self) -> Element {
38- let cb = self.on_press?;300+ let mut builder = Builder::new();
39- let mut buf = [0u8; OUT_LEN];301+ if let Some(cb) = self.on_view {
40- unsafe { cb(self.ctx, button_id, buf.as_mut_ptr().cast(), OUT_LEN) };302+ unsafe { cb(self.ctx, &mut builder) };
41- let end = buf.iter().position(|&b| b == 0).unwrap_or(OUT_LEN);303+ }
42- Some(String::from_utf8_lossy(&buf[..end]).into_owned())304+ builder.finish()
305+ }
306+
307+ fn press(&self, id: i32) {
308+ if let Some(cb) = self.on_press {
309+ unsafe { cb(self.ctx, id) };
310+ }
43 }311 }
44 }312 }
45 313
46 struct Flags {314 struct Flags {
47 title: String,315 title: String,
48- text: String,
49- left: String,
50- right: String,
51 host: Host,316 host: Host,
52 }317 }
53 318
54-#[derive(Debug, Clone, Copy)]
55-enum Message {
56- Pressed(i32),
57-}
58-
59 struct App {319 struct App {
60 core: cosmic::app::Core,320 core: cosmic::app::Core,
61 flags: Flags,321 flags: Flags,
@@ -81,45 +341,18 @@ impl cosmic::Application for App {
81 }341 }
82 342
83 fn header_start(&self) -> Vec<cosmic::Element<Message>> {343 fn header_start(&self) -> Vec<cosmic::Element<Message>> {
84- vec![cosmic::widget::text::heading(self.flags.title.clone()).into()]344+ vec![widget::text::heading(self.flags.title.clone()).into()]
85 }345 }
86 346
87 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {347 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {
88 match message {348 match message {
89- Message::Pressed(id) => {349+ Message::Pressed(id) => self.flags.host.press(id),
90- if let Some(text) = self.flags.host.press(id) {
91- self.flags.text = text;
92- }
93- }
94 }350 }
95 cosmic::app::Task::none()351 cosmic::app::Task::none()
96 }352 }
97 353
98 fn view(&self) -> cosmic::Element<Message> {354 fn view(&self) -> cosmic::Element<Message> {
99- use cosmic::widget;355+ self.flags.host.view()
100-
101- let spacing = cosmic::theme::active().cosmic().spacing;
102-
103- let buttons = widget::row::with_children(vec![
104- widget::button::standard(self.flags.left.clone())
105- .on_press(Message::Pressed(0))
106- .into(),
107- widget::button::suggested(self.flags.right.clone())
108- .on_press(Message::Pressed(1))
109- .into(),
110- ])
111- .spacing(spacing.space_s);
112-
113- widget::container(
114- widget::column::with_children(vec![
115- widget::text::title1(self.flags.text.clone()).into(),
116- buttons.into(),
117- ])
118- .spacing(spacing.space_m)
119- .align_x(cosmic::iced::Alignment::Center),
120- )
121- .center(cosmic::iced::Length::Fill)
122- .into()
123 }356 }
124 }357 }
125 358
@@ -135,28 +368,28 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String {
135 ///368 ///
136 /// # Safety369 /// # Safety
137 /// `config` must point to a valid `CosmicConfig` whose strings are370 /// `config` must point to a valid `CosmicConfig` whose strings are
138-/// NUL-terminated and live until this call returns.371+/// NUL-terminated and live until this call returns. Must be called from the
372+/// main thread.
139 #[no_mangle]373 #[no_mangle]
140 pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 {374 pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 {
141- if config.is_null() {375+ let Some(c) = config.as_ref() else {
142 return -1;376 return -1;
143- }377+ };
144- let c = &*config;
145 378
146 let flags = Flags {379 let flags = Flags {
147 title: str_or(c.title, "Cosmic"),380 title: str_or(c.title, "Cosmic"),
148- text: str_or(c.initial_text, ""),
149- left: str_or(c.left_label, "-"),
150- right: str_or(c.right_label, "+"),
151 host: Host {381 host: Host {
382+ on_view: c.on_view,
152 on_press: c.on_press,383 on_press: c.on_press,
153 ctx: c.ctx,384 ctx: c.ctx,
154 },385 },
155 };386 };
156 387
157- let settings = cosmic::app::Settings::default()388+ let size = cosmic::iced::Size::new(
158- .size(cosmic::iced::Size::new(420.0, 260.0))389+ if c.width == 0 { 420.0 } else { c.width as f32 },
159- .debug(false);390+ if c.height == 0 { 260.0 } else { c.height as f32 },
391+ );
392+ let settings = cosmic::app::Settings::default().size(size).debug(false);
160 393
161 match cosmic::app::run::<App>(settings, flags) {394 match cosmic::app::run::<App>(settings, flags) {
162 Ok(()) => 0,395 Ok(()) => 0,
added justfile +48 -0
new file mode 100644
@@ -0,0 +1,48 @@
1+# cosmicnim tasks. `just` on its own runs the demo.
2+
3+so := "nim/libcosmic_ffi.so"
4+
5+default: run
6+
7+# Run the Nim demo. Needs nim/libcosmic_ffi.so (see `fetch` or `build`).
8+run: _have-so
9+ cd nim && nim c -r app.nim
10+
11+# Type-check the Nim side. Needs no .so: the binding loads it lazily.
12+check:
13+ cd nim && nim check app.nim
14+
15+# Build the cdylib from source into nim/ (cold builds are slow; CI is faster)
16+build:
17+ cd cosmic_ffi && cargo build --release
18+ cp cosmic_ffi/target/release/libcosmic_ffi.so {{so}}
19+
20+# Check the Rust without producing a library.
21+rust-check:
22+ cd cosmic_ffi && cargo clippy --release -- -D warnings
23+
24+fmt:
25+ cd cosmic_ffi && cargo fmt
26+
27+# Install a release tarball's .so from its asset URL (needs $RICKUB_TOKEN)
28+fetch url:
29+ #!/usr/bin/env bash
30+ set -euo pipefail
31+ : "${RICKUB_TOKEN:?export RICKUB_TOKEN with a token that can read releases}"
32+ tmp=$(mktemp -d)
33+ trap 'rm -rf "$tmp"' EXIT
34+ curl -fsSL -H "Authorization: Bearer $RICKUB_TOKEN" -o "$tmp/asset.tar.gz" "{{url}}"
35+ tar xzf "$tmp/asset.tar.gz" -C "$tmp"
36+ cp "$tmp"/cosmic_ffi-*/libcosmic_ffi.so {{so}}
37+ @echo "installed {{so}}"
38+
39+# Cut a release by pushing a tag with git (an API-made tag triggers no CI)
40+tag version:
41+ git tag -a "{{version}}" -m "{{version}}"
42+ git push origin "{{version}}"
43+
44+clean:
45+ rm -rf cosmic_ffi/target nim/nimcache nim/app
46+
47+_have-so:
48+ @test -f {{so}} || { echo "missing {{so}} -- run 'just build' or 'just fetch <url>'" >&2; exit 1; }
new file mode 100644
@@ -0,0 +1,48 @@
1+# cosmicnim tasks. `just` on its own runs the demo.
2+
3+so := "nim/libcosmic_ffi.so"
4+
5+default: run
6+
7+# Run the Nim demo. Needs nim/libcosmic_ffi.so (see `fetch` or `build`).
8+run: _have-so
9+ cd nim && nim c -r app.nim
10+
11+# Type-check the Nim side. Needs no .so: the binding loads it lazily.
12+check:
13+ cd nim && nim check app.nim
14+
15+# Build the cdylib from source into nim/ (cold builds are slow; CI is faster)
16+build:
17+ cd cosmic_ffi && cargo build --release
18+ cp cosmic_ffi/target/release/libcosmic_ffi.so {{so}}
19+
20+# Check the Rust without producing a library.
21+rust-check:
22+ cd cosmic_ffi && cargo clippy --release -- -D warnings
23+
24+fmt:
25+ cd cosmic_ffi && cargo fmt
26+
27+# Install a release tarball's .so from its asset URL (needs $RICKUB_TOKEN)
28+fetch url:
29+ #!/usr/bin/env bash
30+ set -euo pipefail
31+ : "${RICKUB_TOKEN:?export RICKUB_TOKEN with a token that can read releases}"
32+ tmp=$(mktemp -d)
33+ trap 'rm -rf "$tmp"' EXIT
34+ curl -fsSL -H "Authorization: Bearer $RICKUB_TOKEN" -o "$tmp/asset.tar.gz" "{{url}}"
35+ tar xzf "$tmp/asset.tar.gz" -C "$tmp"
36+ cp "$tmp"/cosmic_ffi-*/libcosmic_ffi.so {{so}}
37+ @echo "installed {{so}}"
38+
39+# Cut a release by pushing a tag with git (an API-made tag triggers no CI)
40+tag version:
41+ git tag -a "{{version}}" -m "{{version}}"
42+ git push origin "{{version}}"
43+
44+clean:
45+ rm -rf cosmic_ffi/target nim/nimcache nim/app
46+
47+_have-so:
48+ @test -f {{so}} || { echo "missing {{so}} -- run 'just build' or 'just fetch <url>'" >&2; exit 1; }
modified nim/app.nim +61 -11
@@ -1,26 +1,76 @@
1-## A counter whose state lives in Nim; libcosmic only draws it.
1+## A counter whose state *and layout* live in Nim; libcosmic only draws it.
2+##
3+## The tree is rebuilt from scratch every frame, so it can depend on the
4+## state: reset is disabled at zero, and the history only appears once
5+## there is some.
26
3-import std/strformat
7+import std/[strformat, strutils]
48 import cosmic
59
10+const
11+ IdDec = 0'i32
12+ IdInc = 1'i32
13+ IdReset = 2'i32
14+ IdStepDown = 3'i32
15+ IdStepUp = 4'i32
16+
617 type Counter = object
718 value: int
19+ step: int
20+ history: seq[int]
21+
22+proc apply(c: var Counter; delta: int) =
23+ c.history.add c.value
24+ if c.history.len > 5:
25+ c.history.delete(0)
26+ c.value += delta
27+
28+proc onPress(ctx: pointer; id: int32) {.cdecl.} =
29+ let c = cast[ptr Counter](ctx)
30+ case id
31+ of IdDec: c[].apply(-c.step)
32+ of IdInc: c[].apply(c.step)
33+ of IdReset: c[].apply(-c.value)
34+ of IdStepDown: c.step = max(1, c.step div 2)
35+ of IdStepUp: c.step = min(100, c.step * 2)
36+ else: discard
37+
38+proc onView(ctx: pointer; b: Builder) {.cdecl.} =
39+ let c = cast[ptr Counter](ctx)
40+
41+ b.container:
42+ b.fill()
43+ b.alignCenter()
44+ b.spacing(space(SpaceM))
45+
46+ b.text(&"{c.value}", TextTitle1)
47+
48+ b.row:
49+ b.spacing(space(SpaceS))
50+ b.alignCenter()
51+ b.button(&"−{c.step}", IdDec)
52+ b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
53+ b.button(&"+{c.step}", IdInc, ButtonSuggested)
54+
55+ b.row:
56+ b.spacing(space(SpaceXs))
57+ b.alignCenter()
58+ b.button("÷2", IdStepDown, ButtonText, enabled = c.step > 1)
59+ b.text(&"step {c.step}", TextCaption)
60+ b.button("×2", IdStepUp, ButtonText, enabled = c.step < 100)
861
9-proc onPress(ctx: pointer; buttonId: int32; outBuf: cstring;
10- outLen: csize_t) {.cdecl.} =
11- let counter = cast[ptr Counter](ctx)
12- counter.value += (if buttonId == 0: -1 else: 1)
13- setOut(outBuf, outLen, &"Count: {counter.value}")
62+ if c.history.len > 0:
63+ b.text(c.history.join("") & "" & $c.value, TextCaption)
1464
1565 proc main() =
16- var counter = Counter(value: 0)
66+ var counter = Counter(value: 0, step: 1)
1767 var config = CosmicConfig(
1868 title: "Nim ❤ COSMIC",
19- initialText: "Count: 0",
20- leftLabel: "",
21- rightLabel: "+",
69+ onView: onView,
2270 onPress: onPress,
2371 ctx: addr counter,
72+ width: 460,
73+ height: 320,
2474 )
2575 let rc = cosmicRun(addr config)
2676 if rc != 0:
@@ -1,26 +1,76 @@
1-## A counter whose state lives in Nim; libcosmic only draws it.1+## A counter whose state *and layout* live in Nim; libcosmic only draws it.
2+##
3+## The tree is rebuilt from scratch every frame, so it can depend on the
4+## state: reset is disabled at zero, and the history only appears once
5+## there is some.
2 6
3-import std/strformat7+import std/[strformat, strutils]
4 import cosmic8 import cosmic
5 9
10+const
11+ IdDec = 0'i32
12+ IdInc = 1'i32
13+ IdReset = 2'i32
14+ IdStepDown = 3'i32
15+ IdStepUp = 4'i32
16+
6 type Counter = object17 type Counter = object
7 value: int18 value: int
19+ step: int
20+ history: seq[int]
21+
22+proc apply(c: var Counter; delta: int) =
23+ c.history.add c.value
24+ if c.history.len > 5:
25+ c.history.delete(0)
26+ c.value += delta
27+
28+proc onPress(ctx: pointer; id: int32) {.cdecl.} =
29+ let c = cast[ptr Counter](ctx)
30+ case id
31+ of IdDec: c[].apply(-c.step)
32+ of IdInc: c[].apply(c.step)
33+ of IdReset: c[].apply(-c.value)
34+ of IdStepDown: c.step = max(1, c.step div 2)
35+ of IdStepUp: c.step = min(100, c.step * 2)
36+ else: discard
37+
38+proc onView(ctx: pointer; b: Builder) {.cdecl.} =
39+ let c = cast[ptr Counter](ctx)
40+
41+ b.container:
42+ b.fill()
43+ b.alignCenter()
44+ b.spacing(space(SpaceM))
45+
46+ b.text(&"{c.value}", TextTitle1)
47+
48+ b.row:
49+ b.spacing(space(SpaceS))
50+ b.alignCenter()
51+ b.button(&"−{c.step}", IdDec)
52+ b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
53+ b.button(&"+{c.step}", IdInc, ButtonSuggested)
54+
55+ b.row:
56+ b.spacing(space(SpaceXs))
57+ b.alignCenter()
58+ b.button("÷2", IdStepDown, ButtonText, enabled = c.step > 1)
59+ b.text(&"step {c.step}", TextCaption)
60+ b.button("×2", IdStepUp, ButtonText, enabled = c.step < 100)
8 61
9-proc onPress(ctx: pointer; buttonId: int32; outBuf: cstring;62+ if c.history.len > 0:
10- outLen: csize_t) {.cdecl.} =63+ b.text(c.history.join("") & "" & $c.value, TextCaption)
11- let counter = cast[ptr Counter](ctx)
12- counter.value += (if buttonId == 0: -1 else: 1)
13- setOut(outBuf, outLen, &"Count: {counter.value}")
14 64
15 proc main() =65 proc main() =
16- var counter = Counter(value: 0)66+ var counter = Counter(value: 0, step: 1)
17 var config = CosmicConfig(67 var config = CosmicConfig(
18 title: "Nim ❤ COSMIC",68 title: "Nim ❤ COSMIC",
19- initialText: "Count: 0",69+ onView: onView,
20- leftLabel: "",
21- rightLabel: "+",
22 onPress: onPress,70 onPress: onPress,
23 ctx: addr counter,71 ctx: addr counter,
72+ width: 460,
73+ height: 320,
24 )74 )
25 let rc = cosmicRun(addr config)75 let rc = cosmicRun(addr config)
26 if rc != 0:76 if rc != 0:
modified nim/cosmic.nim +96 -12
@@ -1,26 +1,110 @@
11 ## Nim bindings for the cosmic_ffi cdylib.
2+##
3+## The window's shape is described from Nim, once per frame, inside an
4+## `onView` callback. Containers are block templates, so the widget tree in
5+## the source has the same shape as the widget tree on screen:
6+##
7+## ```nim
8+## b.container:
9+## b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
10+## b.text("Count: 3", TextTitle1)
11+## b.row:
12+## b.spacing(space(SpaceS))
13+## b.button("−", id = 0)
14+## b.button("+", id = 1, style = ButtonSuggested)
15+## ```
216
317 const libCosmicFfi* = "libcosmic_ffi.so"
418
519 type
6- OnPress* = proc (ctx: pointer; buttonId: int32; outBuf: cstring;
7- outLen: csize_t) {.cdecl.}
20+ Builder* = distinct pointer
21+ ## Opaque. Only valid for the duration of the `onView` call it arrived in.
22+
23+ OnView* = proc (ctx: pointer; b: Builder) {.cdecl.}
24+ OnPress* = proc (ctx: pointer; id: int32) {.cdecl.}
825
926 CosmicConfig* = object
1027 title*: cstring
11- initialText*: cstring
12- leftLabel*: cstring
13- rightLabel*: cstring
28+ onView*: OnView
1429 onPress*: OnPress
1530 ctx*: pointer
31+ width*: uint32 ## 0 for a default
32+ height*: uint32 ## 0 for a default
33+
34+ TextStyle* = enum
35+ TextBody, TextTitle1, TextTitle2, TextTitle3, TextTitle4,
36+ TextHeading, TextCaption, TextMonotext
37+
38+ ButtonStyle* = enum
39+ ButtonStandard, ButtonSuggested, ButtonDestructive, ButtonText, ButtonLink
40+
41+ SpaceStep* = enum
42+ SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl
1643
1744 proc cosmicRun*(config: ptr CosmicConfig): int32
1845 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}
1946
20-proc setOut*(outBuf: cstring; outLen: csize_t; s: string) =
21- ## Copy `s` into the callback's output buffer, truncating and always
22- ## leaving room for the terminating NUL.
23- let n = min(s.len, int(outLen) - 1)
24- if n > 0:
25- copyMem(outBuf, unsafeAddr s[0], n)
26- cast[ptr char](cast[uint](outBuf) + uint(n))[] = '\0'
47+# --- raw builder calls; prefer the wrappers below ---
48+
49+proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.}
50+proc beginRow(b: Builder) {.cdecl, importc: "cosmic_row", dynlib: libCosmicFfi.}
51+proc beginContainer(b: Builder) {.cdecl, importc: "cosmic_container", dynlib: libCosmicFfi.}
52+proc endNode(b: Builder) {.cdecl, importc: "cosmic_end", dynlib: libCosmicFfi.}
53+
54+proc rawSpacing(b: Builder; px: cfloat) {.cdecl, importc: "cosmic_spacing", dynlib: libCosmicFfi.}
55+proc rawPadding(b: Builder; px: cfloat) {.cdecl, importc: "cosmic_padding", dynlib: libCosmicFfi.}
56+proc rawAlignCenter(b: Builder) {.cdecl, importc: "cosmic_align_center", dynlib: libCosmicFfi.}
57+proc rawFill(b: Builder) {.cdecl, importc: "cosmic_fill", dynlib: libCosmicFfi.}
58+
59+proc rawText(b: Builder; style: int32; text: cstring)
60+ {.cdecl, importc: "cosmic_text", dynlib: libCosmicFfi.}
61+proc rawButton(b: Builder; style: int32; label: cstring; id: int32)
62+ {.cdecl, importc: "cosmic_button", dynlib: libCosmicFfi.}
63+proc rawSpace(b: Builder; w, h: cfloat)
64+ {.cdecl, importc: "cosmic_space", dynlib: libCosmicFfi.}
65+proc rawSpaceUnit(step: int32): cfloat
66+ {.cdecl, importc: "cosmic_space_unit", dynlib: libCosmicFfi.}
67+
68+# --- containers ---
69+
70+template column*(b: Builder; body: untyped) =
71+ ## Stack the widgets built in `body` vertically.
72+ beginColumn(b)
73+ body
74+ endNode(b)
75+
76+template row*(b: Builder; body: untyped) =
77+ ## Lay the widgets built in `body` out horizontally.
78+ beginRow(b)
79+ body
80+ endNode(b)
81+
82+template container*(b: Builder; body: untyped) =
83+ ## Wrap the widgets built in `body`, typically to pad or centre them.
84+ beginContainer(b)
85+ body
86+ endNode(b)
87+
88+# --- attributes of the innermost open container ---
89+
90+proc spacing*(b: Builder; px: float) = rawSpacing(b, cfloat(px))
91+proc padding*(b: Builder; px: float) = rawPadding(b, cfloat(px))
92+proc alignCenter*(b: Builder) = rawAlignCenter(b)
93+proc fill*(b: Builder) = rawFill(b)
94+
95+# --- leaves ---
96+
97+proc text*(b: Builder; s: string; style = TextBody) =
98+ rawText(b, int32(ord(style)), s.cstring)
99+
100+proc button*(b: Builder; label: string; id: int32; style = ButtonStandard;
101+ enabled = true) =
102+ ## A button that reports `id` back through `onPress`. `enabled = false`
103+ ## draws it inert; `id` is then never delivered.
104+ rawButton(b, int32(ord(style)), label.cstring, if enabled: id else: -1)
105+
106+proc space*(b: Builder; w, h: float) = rawSpace(b, cfloat(w), cfloat(h))
107+
108+proc space*(step: SpaceStep): float =
109+ ## The active COSMIC theme's spacing for `step`, in pixels.
110+ float(rawSpaceUnit(int32(ord(step))))
@@ -1,26 +1,110 @@
1 ## Nim bindings for the cosmic_ffi cdylib.1 ## Nim bindings for the cosmic_ffi cdylib.
2+##
3+## The window's shape is described from Nim, once per frame, inside an
4+## `onView` callback. Containers are block templates, so the widget tree in
5+## the source has the same shape as the widget tree on screen:
6+##
7+## ```nim
8+## b.container:
9+## b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
10+## b.text("Count: 3", TextTitle1)
11+## b.row:
12+## b.spacing(space(SpaceS))
13+## b.button("−", id = 0)
14+## b.button("+", id = 1, style = ButtonSuggested)
15+## ```
2 16
3 const libCosmicFfi* = "libcosmic_ffi.so"17 const libCosmicFfi* = "libcosmic_ffi.so"
4 18
5 type19 type
6- OnPress* = proc (ctx: pointer; buttonId: int32; outBuf: cstring;20+ Builder* = distinct pointer
7- outLen: csize_t) {.cdecl.}21+ ## Opaque. Only valid for the duration of the `onView` call it arrived in.
22+
23+ OnView* = proc (ctx: pointer; b: Builder) {.cdecl.}
24+ OnPress* = proc (ctx: pointer; id: int32) {.cdecl.}
8 25
9 CosmicConfig* = object26 CosmicConfig* = object
10 title*: cstring27 title*: cstring
11- initialText*: cstring28+ onView*: OnView
12- leftLabel*: cstring
13- rightLabel*: cstring
14 onPress*: OnPress29 onPress*: OnPress
15 ctx*: pointer30 ctx*: pointer
31+ width*: uint32 ## 0 for a default
32+ height*: uint32 ## 0 for a default
33+
34+ TextStyle* = enum
35+ TextBody, TextTitle1, TextTitle2, TextTitle3, TextTitle4,
36+ TextHeading, TextCaption, TextMonotext
37+
38+ ButtonStyle* = enum
39+ ButtonStandard, ButtonSuggested, ButtonDestructive, ButtonText, ButtonLink
40+
41+ SpaceStep* = enum
42+ SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl
16 43
17 proc cosmicRun*(config: ptr CosmicConfig): int3244 proc cosmicRun*(config: ptr CosmicConfig): int32
18 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}45 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}
19 46
20-proc setOut*(outBuf: cstring; outLen: csize_t; s: string) =47+# --- raw builder calls; prefer the wrappers below ---
21- ## Copy `s` into the callback's output buffer, truncating and always48+
22- ## leaving room for the terminating NUL.49+proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.}
23- let n = min(s.len, int(outLen) - 1)50+proc beginRow(b: Builder) {.cdecl, importc: "cosmic_row", dynlib: libCosmicFfi.}
24- if n > 0:51+proc beginContainer(b: Builder) {.cdecl, importc: "cosmic_container", dynlib: libCosmicFfi.}
25- copyMem(outBuf, unsafeAddr s[0], n)52+proc endNode(b: Builder) {.cdecl, importc: "cosmic_end", dynlib: libCosmicFfi.}
26- cast[ptr char](cast[uint](outBuf) + uint(n))[] = '\0'53+
54+proc rawSpacing(b: Builder; px: cfloat) {.cdecl, importc: "cosmic_spacing", dynlib: libCosmicFfi.}
55+proc rawPadding(b: Builder; px: cfloat) {.cdecl, importc: "cosmic_padding", dynlib: libCosmicFfi.}
56+proc rawAlignCenter(b: Builder) {.cdecl, importc: "cosmic_align_center", dynlib: libCosmicFfi.}
57+proc rawFill(b: Builder) {.cdecl, importc: "cosmic_fill", dynlib: libCosmicFfi.}
58+
59+proc rawText(b: Builder; style: int32; text: cstring)
60+ {.cdecl, importc: "cosmic_text", dynlib: libCosmicFfi.}
61+proc rawButton(b: Builder; style: int32; label: cstring; id: int32)
62+ {.cdecl, importc: "cosmic_button", dynlib: libCosmicFfi.}
63+proc rawSpace(b: Builder; w, h: cfloat)
64+ {.cdecl, importc: "cosmic_space", dynlib: libCosmicFfi.}
65+proc rawSpaceUnit(step: int32): cfloat
66+ {.cdecl, importc: "cosmic_space_unit", dynlib: libCosmicFfi.}
67+
68+# --- containers ---
69+
70+template column*(b: Builder; body: untyped) =
71+ ## Stack the widgets built in `body` vertically.
72+ beginColumn(b)
73+ body
74+ endNode(b)
75+
76+template row*(b: Builder; body: untyped) =
77+ ## Lay the widgets built in `body` out horizontally.
78+ beginRow(b)
79+ body
80+ endNode(b)
81+
82+template container*(b: Builder; body: untyped) =
83+ ## Wrap the widgets built in `body`, typically to pad or centre them.
84+ beginContainer(b)
85+ body
86+ endNode(b)
87+
88+# --- attributes of the innermost open container ---
89+
90+proc spacing*(b: Builder; px: float) = rawSpacing(b, cfloat(px))
91+proc padding*(b: Builder; px: float) = rawPadding(b, cfloat(px))
92+proc alignCenter*(b: Builder) = rawAlignCenter(b)
93+proc fill*(b: Builder) = rawFill(b)
94+
95+# --- leaves ---
96+
97+proc text*(b: Builder; s: string; style = TextBody) =
98+ rawText(b, int32(ord(style)), s.cstring)
99+
100+proc button*(b: Builder; label: string; id: int32; style = ButtonStandard;
101+ enabled = true) =
102+ ## A button that reports `id` back through `onPress`. `enabled = false`
103+ ## draws it inert; `id` is then never delivered.
104+ rawButton(b, int32(ord(style)), label.cstring, if enabled: id else: -1)
105+
106+proc space*(b: Builder; w, h: float) = rawSpace(b, cfloat(w), cfloat(h))
107+
108+proc space*(step: SpaceStep): float =
109+ ## The active COSMIC theme's spacing for `step`, in pixels.
110+ float(rawSpaceUnit(int32(ord(step))))