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>
18ec8fd parent: 4bb6568 modified
README.md +48 -14 | @@ -5,40 +5,74 @@ | ||
| 5 | 5 | libcosmic's API is generics, traits and closures, so it has no C ABI of its own — |
| 6 | 6 | building libcosmic itself as a `cdylib` exports nothing. The `cosmic_ffi` crate |
| 7 | 7 | 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 | 11 | cosmic_ffi/ Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h |
| 12 | 12 | nim/ Nim bindings (cosmic.nim) and the demo app (app.nim) |
| 13 | +justfile build / run / fetch tasks | |
| 13 | 14 | ``` |
| 14 | 15 | |
| 15 | 16 | ## The contract |
| 16 | 17 | |
| 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. | |
| 20 | 27 | |
| 21 | 28 | ```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); | |
| 24 | 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 | 39 | `cosmic_run` must be called from the main thread, and `ctx` is only ever |
| 28 | 40 | touched from that thread. |
| 29 | 41 | |
| 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: | |
| 31 | 62 | |
| 32 | 63 | ```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 | |
| 36 | 67 | ``` |
| 37 | 68 | |
| 38 | -## Building from source | |
| 69 | +or build it yourself — the cold build is long, which is what the release CI is | |
| 70 | +for: | |
| 39 | 71 | |
| 40 | 72 | ```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 | |
| 44 | 75 | ``` |
| 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` crate | 6 | 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 Rust | 7 | 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.h | 11 | 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 contract | 16 | ## The contract |
| 16 | 17 | ||
| 17 | -Rust owns the widget tree; the host owns the application state. Each button | 18 | +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 the | 19 | +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 | ```c | 28 | ```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 ever | 39 | `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 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: | ||
| 31 | 62 | ||
| 32 | ```bash | 63 | ```bash |
| 33 | -tar xzf cosmic_ffi-<version>-x86_64-unknown-linux-gnu.tar.gz | 64 | +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.nim | 66 | +just run |
| 36 | ``` | 67 | ``` |
| 37 | 68 | ||
| 38 | -## Building from source | 69 | +or build it yourself — the cold build is long, which is what the release CI is |
| 70 | +for: | ||
| 39 | 71 | ||
| 40 | ```bash | 72 | ```bash |
| 41 | -cd cosmic_ffi && cargo build --release | 73 | +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 = [ | ||
| 964 | 964 | |
| 965 | 965 | [[package]] |
| 966 | 966 | name = "cosmic_ffi" |
| 967 | -version = "0.1.0" | |
| 967 | +version = "0.2.0" | |
| 968 | 968 | dependencies = [ |
| 969 | 969 | "libcosmic", |
| 970 | 970 | ] |
| @@ -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 @@ | ||
| 1 | 1 | [package] |
| 2 | 2 | name = "cosmic_ffi" |
| 3 | -version = "0.1.0" | |
| 3 | +version = "0.2.0" | |
| 4 | 4 | edition = "2021" |
| 5 | 5 | |
| 6 | 6 | [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. */ | |
| 2 | 7 | #ifndef COSMIC_FFI_H |
| 3 | 8 | #define COSMIC_FFI_H |
| 4 | 9 | |
| 5 | 10 | #include <stddef.h> |
| 6 | 11 | #include <stdint.h> |
| 7 | 12 | |
| 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); | |
| 12 | 18 | |
| 13 | 19 | typedef struct { |
| 14 | 20 | const char *title; |
| 15 | - const char *initial_text; | |
| 16 | - const char *left_label; | |
| 17 | - const char *right_label; | |
| 21 | + cosmic_on_view on_view; | |
| 18 | 22 | cosmic_on_press on_press; |
| 19 | 23 | void *ctx; |
| 24 | + uint32_t width; /* 0 for a default */ | |
| 25 | + uint32_t height; /* 0 for a default */ | |
| 20 | 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 | 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 | 77 | #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_H | 7 | #ifndef COSMIC_FFI_H |
| 3 | #define COSMIC_FFI_H | 8 | #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 to | 13 | +/* 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 | #endif | 77 | #endif |
modified
cosmic_ffi/src/lib.rs +299 -66 | @@ -1,30 +1,292 @@ | ||
| 1 | 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. | |
| 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. | |
| 6 | 12 | |
| 7 | 13 | use std::ffi::{c_char, c_void, CStr}; |
| 8 | 14 | |
| 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)>; | |
| 13 | 24 | |
| 14 | 25 | #[repr(C)] |
| 15 | 26 | pub struct CosmicConfig { |
| 16 | 27 | 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, | |
| 20 | 29 | pub on_press: OnPress, |
| 21 | 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 | 288 | struct Host { |
| 289 | + on_view: OnView, | |
| 28 | 290 | on_press: OnPress, |
| 29 | 291 | ctx: *mut c_void, |
| 30 | 292 | } |
| @@ -34,28 +296,26 @@ struct Host { | ||
| 34 | 296 | unsafe impl Send for Host {} |
| 35 | 297 | |
| 36 | 298 | 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 | + } | |
| 43 | 311 | } |
| 44 | 312 | } |
| 45 | 313 | |
| 46 | 314 | struct Flags { |
| 47 | 315 | title: String, |
| 48 | - text: String, | |
| 49 | - left: String, | |
| 50 | - right: String, | |
| 51 | 316 | host: Host, |
| 52 | 317 | } |
| 53 | 318 | |
| 54 | -#[derive(Debug, Clone, Copy)] | |
| 55 | -enum Message { | |
| 56 | - Pressed(i32), | |
| 57 | -} | |
| 58 | - | |
| 59 | 319 | struct App { |
| 60 | 320 | core: cosmic::app::Core, |
| 61 | 321 | flags: Flags, |
| @@ -81,45 +341,18 @@ impl cosmic::Application for App { | ||
| 81 | 341 | } |
| 82 | 342 | |
| 83 | 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 | 347 | fn update(&mut self, message: Message) -> cosmic::app::Task<Message> { |
| 88 | 348 | 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), | |
| 94 | 350 | } |
| 95 | 351 | cosmic::app::Task::none() |
| 96 | 352 | } |
| 97 | 353 | |
| 98 | 354 | 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() | |
| 123 | 356 | } |
| 124 | 357 | } |
| 125 | 358 | |
| @@ -135,28 +368,28 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String { | ||
| 135 | 368 | /// |
| 136 | 369 | /// # Safety |
| 137 | 370 | /// `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 | 373 | #[no_mangle] |
| 140 | 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 | 376 | return -1; |
| 143 | - } | |
| 144 | - let c = &*config; | |
| 377 | + }; | |
| 145 | 378 | |
| 146 | 379 | let flags = Flags { |
| 147 | 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 | 381 | host: Host { |
| 382 | + on_view: c.on_view, | |
| 152 | 383 | on_press: c.on_press, |
| 153 | 384 | ctx: c.ctx, |
| 154 | 385 | }, |
| 155 | 386 | }; |
| 156 | 387 | |
| 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); | |
| 160 | 393 | |
| 161 | 394 | match cosmic::app::run::<App>(settings, flags) { |
| 162 | 395 | 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`, which | 4 | +//! 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 of | 15 | +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 | /// # Safety | 369 | /// # Safety |
| 137 | /// `config` must point to a valid `CosmicConfig` whose strings are | 370 | /// `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. | |
| 2 | 6 | |
| 3 | -import std/strformat | |
| 7 | +import std/[strformat, strutils] | |
| 4 | 8 | 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 | 17 | type Counter = object |
| 7 | 18 | 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; | |
| 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) | |
| 14 | 64 | |
| 15 | 65 | proc main() = |
| 16 | - var counter = Counter(value: 0) | |
| 66 | + var counter = Counter(value: 0, step: 1) | |
| 17 | 67 | var config = CosmicConfig( |
| 18 | 68 | title: "Nim ❤ COSMIC", |
| 19 | - initialText: "Count: 0", | |
| 20 | - leftLabel: "−", | |
| 21 | - rightLabel: "+", | |
| 69 | + onView: onView, | |
| 22 | 70 | onPress: onPress, |
| 23 | 71 | ctx: addr counter, |
| 72 | + width: 460, | |
| 73 | + height: 320, | |
| 24 | 74 | ) |
| 25 | 75 | let rc = cosmicRun(addr config) |
| 26 | 76 | 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/strformat | 7 | +import std/[strformat, strutils] |
| 4 | import cosmic | 8 | 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 = object | 17 | type Counter = object |
| 7 | value: int | 18 | 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 @@ | ||
| 1 | 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 | 17 | const libCosmicFfi* = "libcosmic_ffi.so" |
| 4 | 18 | |
| 5 | 19 | 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.} | |
| 8 | 25 | |
| 9 | 26 | CosmicConfig* = object |
| 10 | 27 | title*: cstring |
| 11 | - initialText*: cstring | |
| 12 | - leftLabel*: cstring | |
| 13 | - rightLabel*: cstring | |
| 28 | + onView*: OnView | |
| 14 | 29 | onPress*: OnPress |
| 15 | 30 | 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 | 44 | proc cosmicRun*(config: ptr CosmicConfig): int32 |
| 18 | 45 | {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.} |
| 19 | 46 | |
| 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 | type | 19 | 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* = object | 26 | CosmicConfig* = object |
| 10 | title*: cstring | 27 | title*: cstring |
| 11 | - initialText*: cstring | 28 | + onView*: OnView |
| 12 | - leftLabel*: cstring | ||
| 13 | - rightLabel*: cstring | ||
| 14 | onPress*: OnPress | 29 | onPress*: OnPress |
| 15 | ctx*: pointer | 30 | 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): int32 | 44 | 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 always | 48 | + |
| 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)))) | ||