Map the keyboard to the keypad, and check the ABI version
on_key reports each press as UTF-8: the typed character for ordinary keys, the key's name otherwise. The calculator accepts both spellings of every operator — the ASCII one a keyboard emits and the typographic one printed on the key — plus Enter, Escape and Backspace, which needed a real operation since a computed result is not a typed entry and cannot be rubbed out. Also adds cosmic_abi_version, because this session hit the reason for it twice. Changing a callback signature leaves every symbol resolvable, so a stale library reads its arguments from whatever is in the registers: the keypad came out with garbage widths and looked like a layout bug for two rounds. The binding now refuses to run on a mismatch and says to fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46a59a5 parent: 2d2278d modified
README.md +15 -1 | @@ -29,11 +29,25 @@ it calls `on_press` with that id, the host mutates its own state, and the next | ||
| 29 | 29 | `on_view` reflects it. |
| 30 | 30 | |
| 31 | 31 | ```c |
| 32 | -typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b); | |
| 32 | +typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h); | |
| 33 | 33 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); |
| 34 | +typedef void (*cosmic_on_key)(void *ctx, const char *key); | |
| 35 | +uint32_t cosmic_abi_version(void); | |
| 34 | 36 | int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */ |
| 35 | 37 | ``` |
| 36 | 38 | |
| 39 | +`on_view` is handed the space actually available, which is not the size the | |
| 40 | +config asked for once a tiling compositor has had its say. `on_key` reports | |
| 41 | +the typed character for ordinary keys and the key's name otherwise. | |
| 42 | + | |
| 43 | +## ABI version | |
| 44 | + | |
| 45 | +Check `cosmic_abi_version` before `cosmic_run`; the Nim binding does it for | |
| 46 | +you and refuses to run on a mismatch. A changed callback signature leaves | |
| 47 | +every symbol resolvable, so without the check a stale library reads its | |
| 48 | +arguments from whatever is in the registers — which looks like a layout bug, | |
| 49 | +not a version problem. | |
| 50 | + | |
| 37 | 51 | Because the tree is rebuilt every frame, it can depend on state: a button |
| 38 | 52 | disappears, or goes inert, simply by not being described that way this time |
| 39 | 53 | round. See `cosmic_ffi.h` for the full list of containers, leaves and |
| @@ -29,11 +29,25 @@ it calls `on_press` with that id, the host mutates its own state, and the next | |||
| 29 | `on_view` reflects it. | 29 | `on_view` reflects it. |
| 30 | 30 | ||
| 31 | ```c | 31 | ```c |
| 32 | -typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b); | 32 | +typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h); |
| 33 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); | 33 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); |
| 34 | +typedef void (*cosmic_on_key)(void *ctx, const char *key); | ||
| 35 | +uint32_t cosmic_abi_version(void); | ||
| 34 | int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */ | 36 | int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */ |
| 35 | ``` | 37 | ``` |
| 36 | 38 | ||
| 39 | +`on_view` is handed the space actually available, which is not the size the | ||
| 40 | +config asked for once a tiling compositor has had its say. `on_key` reports | ||
| 41 | +the typed character for ordinary keys and the key's name otherwise. | ||
| 42 | + | ||
| 43 | +## ABI version | ||
| 44 | + | ||
| 45 | +Check `cosmic_abi_version` before `cosmic_run`; the Nim binding does it for | ||
| 46 | +you and refuses to run on a mismatch. A changed callback signature leaves | ||
| 47 | +every symbol resolvable, so without the check a stale library reads its | ||
| 48 | +arguments from whatever is in the registers — which looks like a layout bug, | ||
| 49 | +not a version problem. | ||
| 50 | + | ||
| 37 | Because the tree is rebuilt every frame, it can depend on state: a button | 51 | Because the tree is rebuilt every frame, it can depend on state: a button |
| 38 | disappears, or goes inert, simply by not being described that way this time | 52 | disappears, or goes inert, simply by not being described that way this time |
| 39 | round. See `cosmic_ffi.h` for the full list of containers, leaves and | 53 | round. See `cosmic_ffi.h` for the full list of containers, leaves and |
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.5.0" | |
| 967 | +version = "0.6.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.5.0" | 967 | +version = "0.6.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.5.0" | |
| 3 | +version = "0.6.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.5.0" | 3 | +version = "0.6.0" |
| 4 | edition = "2021" | 4 | edition = "2021" |
| 5 | 5 | ||
| 6 | [lib] | 6 | [lib] |
modified
cosmic_ffi/cosmic_ffi.h +13 -0 | @@ -20,15 +20,28 @@ typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder, float width, | ||
| 20 | 20 | float height); |
| 21 | 21 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); |
| 22 | 22 | |
| 23 | +/* Each key press, as NUL-terminated UTF-8: the typed character for ordinary | |
| 24 | + keys ("7", "+"), otherwise the key's name ("Enter", "Backspace", | |
| 25 | + "Escape", "ArrowLeft"). Valid for the duration of the call only. */ | |
| 26 | +typedef void (*cosmic_on_key)(void *ctx, const char *key); | |
| 27 | + | |
| 23 | 28 | typedef struct { |
| 24 | 29 | const char *title; |
| 25 | 30 | cosmic_on_view on_view; |
| 26 | 31 | cosmic_on_press on_press; |
| 32 | + cosmic_on_key on_key; /* may be NULL */ | |
| 27 | 33 | void *ctx; |
| 28 | 34 | uint32_t width; /* 0 for a default */ |
| 29 | 35 | uint32_t height; /* 0 for a default */ |
| 30 | 36 | } CosmicConfig; |
| 31 | 37 | |
| 38 | +/* Bumped whenever anything here changes meaning: a new export, or a changed | |
| 39 | + callback signature. A signature change leaves every symbol resolvable, so | |
| 40 | + without checking this a mismatched library reads its arguments from | |
| 41 | + whatever is in the registers. Check it before cosmic_run. */ | |
| 42 | +#define COSMIC_ABI_VERSION 6 | |
| 43 | +uint32_t cosmic_abi_version(void); | |
| 44 | + | |
| 32 | 45 | /* Opens the window and blocks until it closes. 0 on success. |
| 33 | 46 | Must be called from the main thread. */ |
| 34 | 47 | int32_t cosmic_run(const CosmicConfig *config); |
| @@ -20,15 +20,28 @@ typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder, float width, | |||
| 20 | float height); | 20 | float height); |
| 21 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); | 21 | typedef void (*cosmic_on_press)(void *ctx, int32_t id); |
| 22 | 22 | ||
| 23 | +/* Each key press, as NUL-terminated UTF-8: the typed character for ordinary | ||
| 24 | + keys ("7", "+"), otherwise the key's name ("Enter", "Backspace", | ||
| 25 | + "Escape", "ArrowLeft"). Valid for the duration of the call only. */ | ||
| 26 | +typedef void (*cosmic_on_key)(void *ctx, const char *key); | ||
| 27 | + | ||
| 23 | typedef struct { | 28 | typedef struct { |
| 24 | const char *title; | 29 | const char *title; |
| 25 | cosmic_on_view on_view; | 30 | cosmic_on_view on_view; |
| 26 | cosmic_on_press on_press; | 31 | cosmic_on_press on_press; |
| 32 | + cosmic_on_key on_key; /* may be NULL */ | ||
| 27 | void *ctx; | 33 | void *ctx; |
| 28 | uint32_t width; /* 0 for a default */ | 34 | uint32_t width; /* 0 for a default */ |
| 29 | uint32_t height; /* 0 for a default */ | 35 | uint32_t height; /* 0 for a default */ |
| 30 | } CosmicConfig; | 36 | } CosmicConfig; |
| 31 | 37 | ||
| 38 | +/* Bumped whenever anything here changes meaning: a new export, or a changed | ||
| 39 | + callback signature. A signature change leaves every symbol resolvable, so | ||
| 40 | + without checking this a mismatched library reads its arguments from | ||
| 41 | + whatever is in the registers. Check it before cosmic_run. */ | ||
| 42 | +#define COSMIC_ABI_VERSION 6 | ||
| 43 | +uint32_t cosmic_abi_version(void); | ||
| 44 | + | ||
| 32 | /* Opens the window and blocks until it closes. 0 on success. | 45 | /* Opens the window and blocks until it closes. 0 on success. |
| 33 | Must be called from the main thread. */ | 46 | Must be called from the main thread. */ |
| 34 | int32_t cosmic_run(const CosmicConfig *config); | 47 | int32_t cosmic_run(const CosmicConfig *config); |
modified
cosmic_ffi/src/lib.rs +46 -1 | @@ -25,11 +25,17 @@ pub type OnView = | ||
| 25 | 25 | /// Called when the button carrying `id` is pressed. |
| 26 | 26 | pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>; |
| 27 | 27 | |
| 28 | +/// Called on each key press with a NUL-terminated UTF-8 name: the typed | |
| 29 | +/// character for ordinary keys ("7", "+"), otherwise the key's name | |
| 30 | +/// ("Enter", "Backspace", "Escape", "ArrowLeft"). Valid for the call only. | |
| 31 | +pub type OnKey = Option<unsafe extern "C" fn(ctx: *mut c_void, key: *const c_char)>; | |
| 32 | + | |
| 28 | 33 | #[repr(C)] |
| 29 | 34 | pub struct CosmicConfig { |
| 30 | 35 | pub title: *const c_char, |
| 31 | 36 | pub on_view: OnView, |
| 32 | 37 | pub on_press: OnPress, |
| 38 | + pub on_key: OnKey, | |
| 33 | 39 | pub ctx: *mut c_void, |
| 34 | 40 | /// Initial window size. 0 means "pick a default". |
| 35 | 41 | pub width: u32, |
| @@ -38,9 +44,10 @@ pub struct CosmicConfig { | ||
| 38 | 44 | |
| 39 | 45 | type Element = cosmic::Element<'static, Message>; |
| 40 | 46 | |
| 41 | -#[derive(Debug, Clone, Copy)] | |
| 47 | +#[derive(Debug, Clone)] | |
| 42 | 48 | enum Message { |
| 43 | 49 | Pressed(i32), |
| 50 | + Key(String), | |
| 44 | 51 | } |
| 45 | 52 | |
| 46 | 53 | // ---------------------------------------------------------------- builder |
| @@ -378,6 +385,7 @@ pub extern "C" fn cosmic_space_unit(step: i32) -> f32 { | ||
| 378 | 385 | struct Host { |
| 379 | 386 | on_view: OnView, |
| 380 | 387 | on_press: OnPress, |
| 388 | + on_key: OnKey, | |
| 381 | 389 | ctx: *mut c_void, |
| 382 | 390 | } |
| 383 | 391 | |
| @@ -399,6 +407,14 @@ impl Host { | ||
| 399 | 407 | unsafe { cb(self.ctx, id) }; |
| 400 | 408 | } |
| 401 | 409 | } |
| 410 | + | |
| 411 | + fn key(&self, name: &str) { | |
| 412 | + let Some(cb) = self.on_key else { return }; | |
| 413 | + let Ok(c) = std::ffi::CString::new(name) else { | |
| 414 | + return; | |
| 415 | + }; | |
| 416 | + unsafe { cb(self.ctx, c.as_ptr()) }; | |
| 417 | + } | |
| 402 | 418 | } |
| 403 | 419 | |
| 404 | 420 | struct Flags { |
| @@ -437,10 +453,25 @@ impl cosmic::Application for App { | ||
| 437 | 453 | fn update(&mut self, message: Message) -> cosmic::app::Task<Message> { |
| 438 | 454 | match message { |
| 439 | 455 | Message::Pressed(id) => self.flags.host.press(id), |
| 456 | + Message::Key(name) => self.flags.host.key(&name), | |
| 440 | 457 | } |
| 441 | 458 | cosmic::app::Task::none() |
| 442 | 459 | } |
| 443 | 460 | |
| 461 | + fn subscription(&self) -> cosmic::iced::Subscription<Message> { | |
| 462 | + use cosmic::iced::keyboard; | |
| 463 | + keyboard::on_key_press(|key, _modifiers| { | |
| 464 | + let name = match key { | |
| 465 | + keyboard::Key::Character(c) => c.to_string(), | |
| 466 | + // Debug gives the variant name, which is exactly the spelling | |
| 467 | + // a host wants to match on: "Enter", "Backspace", "ArrowUp". | |
| 468 | + keyboard::Key::Named(named) => format!("{named:?}"), | |
| 469 | + _ => String::new(), | |
| 470 | + }; | |
| 471 | + (!name.is_empty()).then_some(Message::Key(name)) | |
| 472 | + }) | |
| 473 | + } | |
| 474 | + | |
| 444 | 475 | fn view(&self) -> cosmic::Element<Message> { |
| 445 | 476 | // `responsive` hands the closure the space actually on offer, which is |
| 446 | 477 | // the only way the host can know it. |
| @@ -456,6 +487,19 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String { | ||
| 456 | 487 | CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string() |
| 457 | 488 | } |
| 458 | 489 | |
| 490 | +/// Bumped whenever anything in this header changes meaning — a new export, | |
| 491 | +/// or a changed callback signature. A signature change keeps every symbol | |
| 492 | +/// resolvable, so without this a mismatched library reads its arguments from | |
| 493 | +/// whatever happens to be in the registers. | |
| 494 | +pub const ABI_VERSION: u32 = 6; | |
| 495 | + | |
| 496 | +/// The ABI this library implements. A host that does not recognise the number | |
| 497 | +/// should refuse to run rather than guess. | |
| 498 | +#[no_mangle] | |
| 499 | +pub extern "C" fn cosmic_abi_version() -> u32 { | |
| 500 | + ABI_VERSION | |
| 501 | +} | |
| 502 | + | |
| 459 | 503 | /// Run the window. Blocks until it closes. Returns 0 on success. |
| 460 | 504 | /// |
| 461 | 505 | /// # Safety |
| @@ -473,6 +517,7 @@ pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 { | ||
| 473 | 517 | host: Host { |
| 474 | 518 | on_view: c.on_view, |
| 475 | 519 | on_press: c.on_press, |
| 520 | + on_key: c.on_key, | |
| 476 | 521 | ctx: c.ctx, |
| 477 | 522 | }, |
| 478 | 523 | }; |
| @@ -25,11 +25,17 @@ pub type OnView = | |||
| 25 | /// Called when the button carrying `id` is pressed. | 25 | /// Called when the button carrying `id` is pressed. |
| 26 | pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>; | 26 | pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>; |
| 27 | 27 | ||
| 28 | +/// Called on each key press with a NUL-terminated UTF-8 name: the typed | ||
| 29 | +/// character for ordinary keys ("7", "+"), otherwise the key's name | ||
| 30 | +/// ("Enter", "Backspace", "Escape", "ArrowLeft"). Valid for the call only. | ||
| 31 | +pub type OnKey = Option<unsafe extern "C" fn(ctx: *mut c_void, key: *const c_char)>; | ||
| 32 | + | ||
| 28 | #[repr(C)] | 33 | #[repr(C)] |
| 29 | pub struct CosmicConfig { | 34 | pub struct CosmicConfig { |
| 30 | pub title: *const c_char, | 35 | pub title: *const c_char, |
| 31 | pub on_view: OnView, | 36 | pub on_view: OnView, |
| 32 | pub on_press: OnPress, | 37 | pub on_press: OnPress, |
| 38 | + pub on_key: OnKey, | ||
| 33 | pub ctx: *mut c_void, | 39 | pub ctx: *mut c_void, |
| 34 | /// Initial window size. 0 means "pick a default". | 40 | /// Initial window size. 0 means "pick a default". |
| 35 | pub width: u32, | 41 | pub width: u32, |
| @@ -38,9 +44,10 @@ pub struct CosmicConfig { | |||
| 38 | 44 | ||
| 39 | type Element = cosmic::Element<'static, Message>; | 45 | type Element = cosmic::Element<'static, Message>; |
| 40 | 46 | ||
| 41 | -#[derive(Debug, Clone, Copy)] | 47 | +#[derive(Debug, Clone)] |
| 42 | enum Message { | 48 | enum Message { |
| 43 | Pressed(i32), | 49 | Pressed(i32), |
| 50 | + Key(String), | ||
| 44 | } | 51 | } |
| 45 | 52 | ||
| 46 | // ---------------------------------------------------------------- builder | 53 | // ---------------------------------------------------------------- builder |
| @@ -378,6 +385,7 @@ pub extern "C" fn cosmic_space_unit(step: i32) -> f32 { | |||
| 378 | struct Host { | 385 | struct Host { |
| 379 | on_view: OnView, | 386 | on_view: OnView, |
| 380 | on_press: OnPress, | 387 | on_press: OnPress, |
| 388 | + on_key: OnKey, | ||
| 381 | ctx: *mut c_void, | 389 | ctx: *mut c_void, |
| 382 | } | 390 | } |
| 383 | 391 | ||
| @@ -399,6 +407,14 @@ impl Host { | |||
| 399 | unsafe { cb(self.ctx, id) }; | 407 | unsafe { cb(self.ctx, id) }; |
| 400 | } | 408 | } |
| 401 | } | 409 | } |
| 410 | + | ||
| 411 | + fn key(&self, name: &str) { | ||
| 412 | + let Some(cb) = self.on_key else { return }; | ||
| 413 | + let Ok(c) = std::ffi::CString::new(name) else { | ||
| 414 | + return; | ||
| 415 | + }; | ||
| 416 | + unsafe { cb(self.ctx, c.as_ptr()) }; | ||
| 417 | + } | ||
| 402 | } | 418 | } |
| 403 | 419 | ||
| 404 | struct Flags { | 420 | struct Flags { |
| @@ -437,10 +453,25 @@ impl cosmic::Application for App { | |||
| 437 | fn update(&mut self, message: Message) -> cosmic::app::Task<Message> { | 453 | fn update(&mut self, message: Message) -> cosmic::app::Task<Message> { |
| 438 | match message { | 454 | match message { |
| 439 | Message::Pressed(id) => self.flags.host.press(id), | 455 | Message::Pressed(id) => self.flags.host.press(id), |
| 456 | + Message::Key(name) => self.flags.host.key(&name), | ||
| 440 | } | 457 | } |
| 441 | cosmic::app::Task::none() | 458 | cosmic::app::Task::none() |
| 442 | } | 459 | } |
| 443 | 460 | ||
| 461 | + fn subscription(&self) -> cosmic::iced::Subscription<Message> { | ||
| 462 | + use cosmic::iced::keyboard; | ||
| 463 | + keyboard::on_key_press(|key, _modifiers| { | ||
| 464 | + let name = match key { | ||
| 465 | + keyboard::Key::Character(c) => c.to_string(), | ||
| 466 | + // Debug gives the variant name, which is exactly the spelling | ||
| 467 | + // a host wants to match on: "Enter", "Backspace", "ArrowUp". | ||
| 468 | + keyboard::Key::Named(named) => format!("{named:?}"), | ||
| 469 | + _ => String::new(), | ||
| 470 | + }; | ||
| 471 | + (!name.is_empty()).then_some(Message::Key(name)) | ||
| 472 | + }) | ||
| 473 | + } | ||
| 474 | + | ||
| 444 | fn view(&self) -> cosmic::Element<Message> { | 475 | fn view(&self) -> cosmic::Element<Message> { |
| 445 | // `responsive` hands the closure the space actually on offer, which is | 476 | // `responsive` hands the closure the space actually on offer, which is |
| 446 | // the only way the host can know it. | 477 | // the only way the host can know it. |
| @@ -456,6 +487,19 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String { | |||
| 456 | CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string() | 487 | CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string() |
| 457 | } | 488 | } |
| 458 | 489 | ||
| 490 | +/// Bumped whenever anything in this header changes meaning — a new export, | ||
| 491 | +/// or a changed callback signature. A signature change keeps every symbol | ||
| 492 | +/// resolvable, so without this a mismatched library reads its arguments from | ||
| 493 | +/// whatever happens to be in the registers. | ||
| 494 | +pub const ABI_VERSION: u32 = 6; | ||
| 495 | + | ||
| 496 | +/// The ABI this library implements. A host that does not recognise the number | ||
| 497 | +/// should refuse to run rather than guess. | ||
| 498 | +#[no_mangle] | ||
| 499 | +pub extern "C" fn cosmic_abi_version() -> u32 { | ||
| 500 | + ABI_VERSION | ||
| 501 | +} | ||
| 502 | + | ||
| 459 | /// Run the window. Blocks until it closes. Returns 0 on success. | 503 | /// Run the window. Blocks until it closes. Returns 0 on success. |
| 460 | /// | 504 | /// |
| 461 | /// # Safety | 505 | /// # Safety |
| @@ -473,6 +517,7 @@ pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 { | |||
| 473 | host: Host { | 517 | host: Host { |
| 474 | on_view: c.on_view, | 518 | on_view: c.on_view, |
| 475 | on_press: c.on_press, | 519 | on_press: c.on_press, |
| 520 | + on_key: c.on_key, | ||
| 476 | ctx: c.ctx, | 521 | ctx: c.ctx, |
| 477 | }, | 522 | }, |
| 478 | }; | 523 | }; |
modified
cosmicnim.nimble +1 -1 | @@ -1,4 +1,4 @@ | ||
| 1 | -version = "0.5.0" | |
| 1 | +version = "0.6.0" | |
| 2 | 2 | author = "nandi" |
| 3 | 3 | description = "libcosmic behind a C ABI, driven from Nim" |
| 4 | 4 | license = "MIT" |
| @@ -1,4 +1,4 @@ | |||
| 1 | -version = "0.5.0" | 1 | +version = "0.6.0" |
| 2 | author = "nandi" | 2 | author = "nandi" |
| 3 | description = "libcosmic behind a C ABI, driven from Nim" | 3 | description = "libcosmic behind a C ABI, driven from Nim" |
| 4 | license = "MIT" | 4 | license = "MIT" |
modified
examples/calculator.nim +39 -2 | @@ -4,6 +4,7 @@ | ||
| 4 | 4 | ## in the source are the rows on screen. State that has no button — a pending |
| 5 | 5 | ## operator, an error — simply changes what gets described next frame. |
| 6 | 6 | |
| 7 | + | |
| 7 | 8 | import std/[math, strformat, strutils] |
| 8 | 9 | import cosmicnim |
| 9 | 10 | |
| @@ -39,6 +40,7 @@ const | ||
| 39 | 40 | IdClear* = 16'i32 |
| 40 | 41 | IdSign* = 17'i32 |
| 41 | 42 | IdPercent* = 18'i32 |
| 43 | + IdBackspace* = 19'i32 | |
| 42 | 44 | |
| 43 | 45 | Keypad: array[5, array[4, Key]] = [ |
| 44 | 46 | [Key(label: "C", id: IdClear, style: ButtonDestructive), |
| @@ -146,12 +148,41 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | ||
| 146 | 148 | else: c.entry = "-" & c.entry |
| 147 | 149 | else: |
| 148 | 150 | c.acc = -c.acc |
| 151 | + of IdBackspace: | |
| 152 | + # Only the typed entry can be rubbed out; a computed result cannot. | |
| 153 | + if c.entry.len > 0: | |
| 154 | + c.entry.setLen(c.entry.len - 1) | |
| 155 | + if c.entry == "-": c.entry = "" | |
| 149 | 156 | of IdPercent: |
| 150 | 157 | if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0) |
| 151 | 158 | else: c.acc = c.acc / 100.0 |
| 152 | 159 | else: |
| 153 | 160 | discard |
| 154 | 161 | |
| 162 | +proc keyToId*(key: string): int32 = | |
| 163 | + ## The id a key press stands for, or -1 for a key the calculator ignores. | |
| 164 | + ## Both spellings of each operator are accepted: the ASCII one a keyboard | |
| 165 | + ## actually produces, and the typographic one printed on the key. | |
| 166 | + if key.len == 1 and key[0] in '0'..'9': | |
| 167 | + return int32(ord(key[0]) - ord('0')) | |
| 168 | + case key | |
| 169 | + of ".", ",": IdDot | |
| 170 | + of "+": IdAdd | |
| 171 | + of "-", "−": IdSub | |
| 172 | + of "*", "x", "×": IdMul | |
| 173 | + of "/", "÷": IdDiv | |
| 174 | + of "=", "Enter", "NumpadEnter": IdEquals | |
| 175 | + of "c", "C", "Escape", "Delete": IdClear | |
| 176 | + of "Backspace": IdBackspace | |
| 177 | + of "%": IdPercent | |
| 178 | + of "n", "N": IdSign | |
| 179 | + else: -1 | |
| 180 | + | |
| 181 | +proc onKey(ctx: pointer; key: cstring) {.cdecl.} = | |
| 182 | + let id = keyToId($key) | |
| 183 | + if id >= 0: | |
| 184 | + onPress(ctx, id) | |
| 185 | + | |
| 155 | 186 | proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = |
| 156 | 187 | let c = cast[ptr Calc](ctx) |
| 157 | 188 | |
| @@ -162,9 +193,14 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = | ||
| 162 | 193 | gap = space(SpaceXs) |
| 163 | 194 | pad = space(SpaceM) |
| 164 | 195 | display = 96.0 |
| 165 | - keyW = max(MinKeyW, (float(width) - 2 * pad - float(Cols - 1) * gap) / float(Cols)) | |
| 196 | + # A dimension that is not a sane finite number means the library could not | |
| 197 | + # say, so fall back to the size the config asked for rather than dividing | |
| 198 | + # by infinity and collapsing every key to its floor. | |
| 199 | + availW = if float(width) > 0 and float(width) < 1.0e5: float(width) else: 420.0 | |
| 200 | + availH = if float(height) > 0 and float(height) < 1.0e5: float(height) else: 640.0 | |
| 201 | + keyW = max(MinKeyW, (availW - 2 * pad - float(Cols - 1) * gap) / float(Cols)) | |
| 166 | 202 | keyH = max(MinKeyH, |
| 167 | - (float(height) - display - 2 * pad - float(Rows - 1) * gap) / float(Rows)) | |
| 203 | + (availH - display - 2 * pad - float(Rows - 1) * gap) / float(Rows)) | |
| 168 | 204 | |
| 169 | 205 | b.container: |
| 170 | 206 | b.fill() |
| @@ -196,6 +232,7 @@ proc main() = | ||
| 196 | 232 | title: "Nim ❤ COSMIC", |
| 197 | 233 | onView: onView, |
| 198 | 234 | onPress: onPress, |
| 235 | + onKey: onKey, | |
| 199 | 236 | ctx: addr calc, |
| 200 | 237 | width: 420, |
| 201 | 238 | height: 640, |
| @@ -4,6 +4,7 @@ | |||
| 4 | ## in the source are the rows on screen. State that has no button — a pending | 4 | ## in the source are the rows on screen. State that has no button — a pending |
| 5 | ## operator, an error — simply changes what gets described next frame. | 5 | ## operator, an error — simply changes what gets described next frame. |
| 6 | 6 | ||
| 7 | + | ||
| 7 | import std/[math, strformat, strutils] | 8 | import std/[math, strformat, strutils] |
| 8 | import cosmicnim | 9 | import cosmicnim |
| 9 | 10 | ||
| @@ -39,6 +40,7 @@ const | |||
| 39 | IdClear* = 16'i32 | 40 | IdClear* = 16'i32 |
| 40 | IdSign* = 17'i32 | 41 | IdSign* = 17'i32 |
| 41 | IdPercent* = 18'i32 | 42 | IdPercent* = 18'i32 |
| 43 | + IdBackspace* = 19'i32 | ||
| 42 | 44 | ||
| 43 | Keypad: array[5, array[4, Key]] = [ | 45 | Keypad: array[5, array[4, Key]] = [ |
| 44 | [Key(label: "C", id: IdClear, style: ButtonDestructive), | 46 | [Key(label: "C", id: IdClear, style: ButtonDestructive), |
| @@ -146,12 +148,41 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} = | |||
| 146 | else: c.entry = "-" & c.entry | 148 | else: c.entry = "-" & c.entry |
| 147 | else: | 149 | else: |
| 148 | c.acc = -c.acc | 150 | c.acc = -c.acc |
| 151 | + of IdBackspace: | ||
| 152 | + # Only the typed entry can be rubbed out; a computed result cannot. | ||
| 153 | + if c.entry.len > 0: | ||
| 154 | + c.entry.setLen(c.entry.len - 1) | ||
| 155 | + if c.entry == "-": c.entry = "" | ||
| 149 | of IdPercent: | 156 | of IdPercent: |
| 150 | if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0) | 157 | if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0) |
| 151 | else: c.acc = c.acc / 100.0 | 158 | else: c.acc = c.acc / 100.0 |
| 152 | else: | 159 | else: |
| 153 | discard | 160 | discard |
| 154 | 161 | ||
| 162 | +proc keyToId*(key: string): int32 = | ||
| 163 | + ## The id a key press stands for, or -1 for a key the calculator ignores. | ||
| 164 | + ## Both spellings of each operator are accepted: the ASCII one a keyboard | ||
| 165 | + ## actually produces, and the typographic one printed on the key. | ||
| 166 | + if key.len == 1 and key[0] in '0'..'9': | ||
| 167 | + return int32(ord(key[0]) - ord('0')) | ||
| 168 | + case key | ||
| 169 | + of ".", ",": IdDot | ||
| 170 | + of "+": IdAdd | ||
| 171 | + of "-", "−": IdSub | ||
| 172 | + of "*", "x", "×": IdMul | ||
| 173 | + of "/", "÷": IdDiv | ||
| 174 | + of "=", "Enter", "NumpadEnter": IdEquals | ||
| 175 | + of "c", "C", "Escape", "Delete": IdClear | ||
| 176 | + of "Backspace": IdBackspace | ||
| 177 | + of "%": IdPercent | ||
| 178 | + of "n", "N": IdSign | ||
| 179 | + else: -1 | ||
| 180 | + | ||
| 181 | +proc onKey(ctx: pointer; key: cstring) {.cdecl.} = | ||
| 182 | + let id = keyToId($key) | ||
| 183 | + if id >= 0: | ||
| 184 | + onPress(ctx, id) | ||
| 185 | + | ||
| 155 | proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = | 186 | proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = |
| 156 | let c = cast[ptr Calc](ctx) | 187 | let c = cast[ptr Calc](ctx) |
| 157 | 188 | ||
| @@ -162,9 +193,14 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} = | |||
| 162 | gap = space(SpaceXs) | 193 | gap = space(SpaceXs) |
| 163 | pad = space(SpaceM) | 194 | pad = space(SpaceM) |
| 164 | display = 96.0 | 195 | display = 96.0 |
| 165 | - keyW = max(MinKeyW, (float(width) - 2 * pad - float(Cols - 1) * gap) / float(Cols)) | 196 | + # A dimension that is not a sane finite number means the library could not |
| 197 | + # say, so fall back to the size the config asked for rather than dividing | ||
| 198 | + # by infinity and collapsing every key to its floor. | ||
| 199 | + availW = if float(width) > 0 and float(width) < 1.0e5: float(width) else: 420.0 | ||
| 200 | + availH = if float(height) > 0 and float(height) < 1.0e5: float(height) else: 640.0 | ||
| 201 | + keyW = max(MinKeyW, (availW - 2 * pad - float(Cols - 1) * gap) / float(Cols)) | ||
| 166 | keyH = max(MinKeyH, | 202 | keyH = max(MinKeyH, |
| 167 | - (float(height) - display - 2 * pad - float(Rows - 1) * gap) / float(Rows)) | 203 | + (availH - display - 2 * pad - float(Rows - 1) * gap) / float(Rows)) |
| 168 | 204 | ||
| 169 | b.container: | 205 | b.container: |
| 170 | b.fill() | 206 | b.fill() |
| @@ -196,6 +232,7 @@ proc main() = | |||
| 196 | title: "Nim ❤ COSMIC", | 232 | title: "Nim ❤ COSMIC", |
| 197 | onView: onView, | 233 | onView: onView, |
| 198 | onPress: onPress, | 234 | onPress: onPress, |
| 235 | + onKey: onKey, | ||
| 199 | ctx: addr calc, | 236 | ctx: addr calc, |
| 200 | width: 420, | 237 | width: 420, |
| 201 | height: 640, | 238 | height: 640, |
modified
nim/cosmicnim.nim +25 -1 | @@ -36,10 +36,16 @@ type | ||
| 36 | 36 | ## CosmicConfig asked for, which a tiling compositor will override. |
| 37 | 37 | OnPress* = proc (ctx: pointer; id: int32) {.cdecl.} |
| 38 | 38 | |
| 39 | + OnKey* = proc (ctx: pointer; key: cstring) {.cdecl.} | |
| 40 | + ## Each key press: the typed character for ordinary keys ("7", "+"), | |
| 41 | + ## otherwise the key's name ("Enter", "Backspace", "Escape", | |
| 42 | + ## "ArrowLeft"). The string is only valid during the call. | |
| 43 | + | |
| 39 | 44 | CosmicConfig* = object |
| 40 | 45 | title*: cstring |
| 41 | 46 | onView*: OnView |
| 42 | 47 | onPress*: OnPress |
| 48 | + onKey*: OnKey ## may be nil | |
| 43 | 49 | ctx*: pointer |
| 44 | 50 | width*: uint32 ## 0 for a default |
| 45 | 51 | height*: uint32 ## 0 for a default |
| @@ -54,9 +60,27 @@ type | ||
| 54 | 60 | SpaceStep* = enum |
| 55 | 61 | SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl |
| 56 | 62 | |
| 57 | -proc cosmicRun*(config: ptr CosmicConfig): int32 | |
| 63 | +const AbiVersion* = 6'u32 | |
| 64 | + ## The ABI these bindings are written against. Bumped whenever a signature | |
| 65 | + ## changes, which is invisible to the dynamic linker: every symbol still | |
| 66 | + ## resolves, and the arguments are simply read from the wrong registers. | |
| 67 | + | |
| 68 | +proc cosmicAbiVersion*(): uint32 | |
| 69 | + {.cdecl, importc: "cosmic_abi_version", dynlib: libCosmicFfi.} | |
| 70 | + | |
| 71 | +proc rawCosmicRun(config: ptr CosmicConfig): int32 | |
| 58 | 72 | {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.} |
| 59 | 73 | |
| 74 | +proc cosmicRun*(config: ptr CosmicConfig): int32 = | |
| 75 | + ## Refuses to run against a library that speaks a different ABI, rather | |
| 76 | + ## than letting the callbacks read whatever is in the registers. | |
| 77 | + let found = cosmicAbiVersion() | |
| 78 | + if found != AbiVersion: | |
| 79 | + raise newException(LibraryError, | |
| 80 | + "libcosmic_ffi speaks ABI " & $found & ", these bindings expect " & | |
| 81 | + $AbiVersion & " -- run `just fetch` to get the matching release") | |
| 82 | + rawCosmicRun(config) | |
| 83 | + | |
| 60 | 84 | # --- raw builder calls; prefer the wrappers below --- |
| 61 | 85 | |
| 62 | 86 | proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.} |
| @@ -36,10 +36,16 @@ type | |||
| 36 | ## CosmicConfig asked for, which a tiling compositor will override. | 36 | ## CosmicConfig asked for, which a tiling compositor will override. |
| 37 | OnPress* = proc (ctx: pointer; id: int32) {.cdecl.} | 37 | OnPress* = proc (ctx: pointer; id: int32) {.cdecl.} |
| 38 | 38 | ||
| 39 | + OnKey* = proc (ctx: pointer; key: cstring) {.cdecl.} | ||
| 40 | + ## Each key press: the typed character for ordinary keys ("7", "+"), | ||
| 41 | + ## otherwise the key's name ("Enter", "Backspace", "Escape", | ||
| 42 | + ## "ArrowLeft"). The string is only valid during the call. | ||
| 43 | + | ||
| 39 | CosmicConfig* = object | 44 | CosmicConfig* = object |
| 40 | title*: cstring | 45 | title*: cstring |
| 41 | onView*: OnView | 46 | onView*: OnView |
| 42 | onPress*: OnPress | 47 | onPress*: OnPress |
| 48 | + onKey*: OnKey ## may be nil | ||
| 43 | ctx*: pointer | 49 | ctx*: pointer |
| 44 | width*: uint32 ## 0 for a default | 50 | width*: uint32 ## 0 for a default |
| 45 | height*: uint32 ## 0 for a default | 51 | height*: uint32 ## 0 for a default |
| @@ -54,9 +60,27 @@ type | |||
| 54 | SpaceStep* = enum | 60 | SpaceStep* = enum |
| 55 | SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl | 61 | SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl |
| 56 | 62 | ||
| 57 | -proc cosmicRun*(config: ptr CosmicConfig): int32 | 63 | +const AbiVersion* = 6'u32 |
| 64 | + ## The ABI these bindings are written against. Bumped whenever a signature | ||
| 65 | + ## changes, which is invisible to the dynamic linker: every symbol still | ||
| 66 | + ## resolves, and the arguments are simply read from the wrong registers. | ||
| 67 | + | ||
| 68 | +proc cosmicAbiVersion*(): uint32 | ||
| 69 | + {.cdecl, importc: "cosmic_abi_version", dynlib: libCosmicFfi.} | ||
| 70 | + | ||
| 71 | +proc rawCosmicRun(config: ptr CosmicConfig): int32 | ||
| 58 | {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.} | 72 | {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.} |
| 59 | 73 | ||
| 74 | +proc cosmicRun*(config: ptr CosmicConfig): int32 = | ||
| 75 | + ## Refuses to run against a library that speaks a different ABI, rather | ||
| 76 | + ## than letting the callbacks read whatever is in the registers. | ||
| 77 | + let found = cosmicAbiVersion() | ||
| 78 | + if found != AbiVersion: | ||
| 79 | + raise newException(LibraryError, | ||
| 80 | + "libcosmic_ffi speaks ABI " & $found & ", these bindings expect " & | ||
| 81 | + $AbiVersion & " -- run `just fetch` to get the matching release") | ||
| 82 | + rawCosmicRun(config) | ||
| 83 | + | ||
| 60 | # --- raw builder calls; prefer the wrappers below --- | 84 | # --- raw builder calls; prefer the wrappers below --- |
| 61 | 85 | ||
| 62 | proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.} | 86 | proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.} |
modified
tests/tcalculator.nim +45 -0 | @@ -69,7 +69,52 @@ suite "calculator": | ||
| 69 | 69 | var c = fresh() |
| 70 | 70 | check c.press(6, IdDiv, 3, IdEquals) == "2" |
| 71 | 71 | |
| 72 | + test "backspace rubs out typed digits only": | |
| 73 | + var c = fresh() | |
| 74 | + check c.press(1, 2, 3, IdBackspace) == "12" | |
| 75 | + check c.press(IdBackspace, IdBackspace) == "0" # empty entry shows acc | |
| 76 | + c = fresh() | |
| 77 | + check c.press(2, IdAdd, 3, IdEquals) == "5" | |
| 78 | + check c.press(IdBackspace) == "5" # a result is not an entry | |
| 79 | + | |
| 72 | 80 | test "a new number replaces the result, not appends to it": |
| 73 | 81 | var c = fresh() |
| 74 | 82 | check c.press(2, IdAdd, 3, IdEquals) == "5" |
| 75 | 83 | check c.press(7) == "7" |
| 84 | + | |
| 85 | +suite "keyboard": | |
| 86 | + proc typed(c: var Calc; keys: varargs[string]): string = | |
| 87 | + for k in keys: | |
| 88 | + let id = keyToId(k) | |
| 89 | + if id >= 0: onPress(addr c, id) | |
| 90 | + c.display | |
| 91 | + | |
| 92 | + test "digits and operators come from the characters you type": | |
| 93 | + var c = fresh() | |
| 94 | + check c.typed("1", "2", "+", "3", "Enter") == "15" | |
| 95 | + | |
| 96 | + test "both spellings of an operator work": | |
| 97 | + var c = fresh() | |
| 98 | + check c.typed("8", "/", "2", "=") == "4" | |
| 99 | + c = fresh() | |
| 100 | + check c.typed("8", "\u00f7", "2", "=") == "4" # the divide sign on the key | |
| 101 | + c = fresh() | |
| 102 | + check c.typed("3", "*", "4", "=") == "12" | |
| 103 | + c = fresh() | |
| 104 | + check c.typed("3", "\u00d7", "4", "=") == "12" | |
| 105 | + | |
| 106 | + test "escape and c both clear": | |
| 107 | + var c = fresh() | |
| 108 | + check c.typed("9", "Escape") == "0" | |
| 109 | + check c.typed("9", "c") == "0" | |
| 110 | + | |
| 111 | + test "backspace is wired to the key": | |
| 112 | + var c = fresh() | |
| 113 | + check c.typed("1", "2", "3", "Backspace") == "12" | |
| 114 | + | |
| 115 | + test "unmapped keys are ignored, not mistaken for a digit": | |
| 116 | + var c = fresh() | |
| 117 | + check keyToId("F1") == -1 | |
| 118 | + check keyToId("ArrowUp") == -1 | |
| 119 | + check keyToId("q") == -1 | |
| 120 | + check c.typed("5", "F1", "ArrowUp", "q") == "5" | |
| @@ -69,7 +69,52 @@ suite "calculator": | |||
| 69 | var c = fresh() | 69 | var c = fresh() |
| 70 | check c.press(6, IdDiv, 3, IdEquals) == "2" | 70 | check c.press(6, IdDiv, 3, IdEquals) == "2" |
| 71 | 71 | ||
| 72 | + test "backspace rubs out typed digits only": | ||
| 73 | + var c = fresh() | ||
| 74 | + check c.press(1, 2, 3, IdBackspace) == "12" | ||
| 75 | + check c.press(IdBackspace, IdBackspace) == "0" # empty entry shows acc | ||
| 76 | + c = fresh() | ||
| 77 | + check c.press(2, IdAdd, 3, IdEquals) == "5" | ||
| 78 | + check c.press(IdBackspace) == "5" # a result is not an entry | ||
| 79 | + | ||
| 72 | test "a new number replaces the result, not appends to it": | 80 | test "a new number replaces the result, not appends to it": |
| 73 | var c = fresh() | 81 | var c = fresh() |
| 74 | check c.press(2, IdAdd, 3, IdEquals) == "5" | 82 | check c.press(2, IdAdd, 3, IdEquals) == "5" |
| 75 | check c.press(7) == "7" | 83 | check c.press(7) == "7" |
| 84 | + | ||
| 85 | +suite "keyboard": | ||
| 86 | + proc typed(c: var Calc; keys: varargs[string]): string = | ||
| 87 | + for k in keys: | ||
| 88 | + let id = keyToId(k) | ||
| 89 | + if id >= 0: onPress(addr c, id) | ||
| 90 | + c.display | ||
| 91 | + | ||
| 92 | + test "digits and operators come from the characters you type": | ||
| 93 | + var c = fresh() | ||
| 94 | + check c.typed("1", "2", "+", "3", "Enter") == "15" | ||
| 95 | + | ||
| 96 | + test "both spellings of an operator work": | ||
| 97 | + var c = fresh() | ||
| 98 | + check c.typed("8", "/", "2", "=") == "4" | ||
| 99 | + c = fresh() | ||
| 100 | + check c.typed("8", "\u00f7", "2", "=") == "4" # the divide sign on the key | ||
| 101 | + c = fresh() | ||
| 102 | + check c.typed("3", "*", "4", "=") == "12" | ||
| 103 | + c = fresh() | ||
| 104 | + check c.typed("3", "\u00d7", "4", "=") == "12" | ||
| 105 | + | ||
| 106 | + test "escape and c both clear": | ||
| 107 | + var c = fresh() | ||
| 108 | + check c.typed("9", "Escape") == "0" | ||
| 109 | + check c.typed("9", "c") == "0" | ||
| 110 | + | ||
| 111 | + test "backspace is wired to the key": | ||
| 112 | + var c = fresh() | ||
| 113 | + check c.typed("1", "2", "3", "Backspace") == "12" | ||
| 114 | + | ||
| 115 | + test "unmapped keys are ignored, not mistaken for a digit": | ||
| 116 | + var c = fresh() | ||
| 117 | + check keyToId("F1") == -1 | ||
| 118 | + check keyToId("ArrowUp") == -1 | ||
| 119 | + check keyToId("q") == -1 | ||
| 120 | + check c.typed("5", "F1", "ArrowUp", "q") == "5" | ||