Compare commits

...

171 Commits

Author SHA1 Message Date
mars 73d524cddf Remove the book 2023-03-28 15:57:05 -04:00
mars f842ae608b Add screenshots to README 2023-03-28 15:40:49 -04:00
mars 3a778c0aa6 Add README 2023-03-28 15:33:43 -04:00
Iris Pupo 38fc390e10 Fix names and paths in usage docs 2023-03-24 17:12:41 -04:00
mars 4b89e781b0 Upgrade wasmtime to 3.0 2022-12-18 13:44:31 -07:00
mars 25eedc0a0d Switch default SAO UI palette to Rose Pine Moon 2022-12-07 20:19:24 -07:00
mars ad0feb2b31 Make SAO UI notification panel prettier 2022-12-07 20:05:21 -07:00
mars 508abcb0cc Replace smol with async-std in music player + notification timeouts 2022-12-07 18:11:10 -07:00
mars 9660ebb4dd Add Magpie ClosePanel message 2022-12-07 18:01:05 -07:00
mars e944caa358 Replace smol with async-std in music player 2022-12-07 16:30:37 -07:00
mars 7adc356c41 Merge pull request 'Notification daemon' (#55) from notifications into main
Reviewed-on: #55
2022-12-07 22:18:19 +00:00
mars 482a0de030 Add notifications support to SAO UI 2022-12-07 15:06:25 -07:00
mars a6b675dd11 Fix is_empty() Option<String> filtering 2022-12-06 17:54:19 -07:00
mars d073dde8ca canary-notifications protocol and panel opening 2022-12-06 17:09:50 -07:00
mars 2d098f8dea Merge branch 'main' into notifications 2022-12-06 17:01:15 -07:00
mars c06cd2d999 Update music player to use init_msg 2022-12-06 16:59:19 -07:00
mars 780f13a015 Add init_msg to Magpie CreatePanel message 2022-12-06 16:59:06 -07:00
mars c951c95650 Merge pull request 'Logging' (#53) from log into main
Reviewed-on: #53
2022-12-06 23:38:58 +00:00
mars c58ca051f4 Magpie logs info and up by default 2022-12-06 16:25:36 -07:00
mars ae7271a479 Add logging macros to Magpie 2022-12-05 21:53:38 -07:00
mars 4b46142f90 Fix use of prehash so that the RAM cache actually hits 2022-12-05 21:26:05 -07:00
mars 03e596b219 Add logging to canary crate 2022-12-05 21:17:19 -07:00
mars b9416b922f Add env_logger to Magpie 2022-12-05 21:01:01 -07:00
mars 14f077ff97 Merge branch 'main' into notifications 2022-12-05 20:27:04 -07:00
mars 109af8793b Merge pull request 'Text wrapping' (#52) from textwrap into main
Reviewed-on: #52
2022-12-06 03:26:45 +00:00
mars 38674c2580 Add configurable text color to textwrap 2022-12-05 20:26:12 -07:00
mars 70f9ca4405 Add configurable textwrap scale and line height 2022-12-05 20:20:33 -07:00
mars c7981b5064 Add Apache 2.0 license to canary-textwrap 2022-12-05 20:09:03 -07:00
mars e4a279c230 Barely-functional Canary text wrapping 2022-12-05 20:00:13 -07:00
mars 9920fea900 Better ID handling in Font + TextLayout 2022-12-05 19:59:46 -07:00
mars feec4de657 Add initial canary-textwrap crate 2022-12-05 14:44:44 -07:00
mars a5f279dfd1 Merge branch 'main' into notifications 2022-12-04 14:46:58 -07:00
mars d62f8f746a Merge pull request 'magpie socket cleanup' (#51) from socket-cleanup into main
Reviewed-on: #51
2022-12-04 21:39:25 +00:00
mars c8a6aa6fbd Cleanup socket cleanup code 2022-12-04 14:39:06 -07:00
Iris Pupo 0768a7dc61 Handle previous sockets in Listener::new() 2022-12-04 01:08:36 -05:00
Iris Pupo 01e2aee164 added new function for listener struct 2022-12-04 00:03:55 -05:00
mars 72d7e703c1 Load Magpie client in notifications 2022-11-29 21:56:59 -07:00
mars ce187a0381 Use find_socket() in music player 2022-11-29 18:01:17 -07:00
mars 4007c40ba6 Add find_socket() to Magpie 2022-11-29 18:01:09 -07:00
mars 0d4eb5d188 Merge branch 'main' into notifications 2022-11-29 17:28:45 -07:00
mars 44d2ac693a Merge pull request 'Fix licenses' (#48) from fix-licenses into main
Reviewed-on: #48
2022-11-27 05:18:31 +00:00
mars f8965b802c Move check_licenses.sh license fetch outside of src/ loop 2022-11-26 22:16:12 -07:00
mars 8947041417 Revert "Fix check_licenses.sh tomcat error failure"
This reverts commit b1b7d1d4e7.
2022-11-26 22:14:48 -07:00
mars 01a37153af Revert "Hacky e-flag manipulation to catch tomcat errors"
This reverts commit b774115d20.
2022-11-26 22:14:34 -07:00
mars 734ad8d630 Add missing license headers to SAO UI sources 2022-11-26 22:06:39 -07:00
mars 6054949694 Add license header to music-player mpris.rs 2022-11-26 22:06:21 -07:00
mars b0c29818ee Add AGPL license to magpie Cargo.toml 2022-11-26 22:06:09 -07:00
mars b774115d20 Hacky e-flag manipulation to catch tomcat errors 2022-11-26 22:03:51 -07:00
mars b1b7d1d4e7 Fix check_licenses.sh tomcat error failure 2022-11-26 21:53:03 -07:00
mars 7ab55cf13b Merge pull request 'wasmtime module caching' (#46) from cache-modules into main
Reviewed-on: #46
2022-11-26 00:54:25 +00:00
mars a9de3ddc5b Merge branch 'main' into cache-modules 2022-11-25 17:54:13 -07:00
mars d43a62dfda Clean up wasmtime caching code 2022-11-25 17:52:24 -07:00
mars 57c6275958 Sort prehash crate dep alphabetically 2022-11-25 17:49:38 -07:00
mars 2bef47a65a Replace music player SetPosition message with relative Seek 2022-11-25 17:48:36 -07:00
Iris Pupo 57768d041a Added module caching 2022-11-25 19:43:58 -05:00
mars 448435eb8c Improve music player slider positioning 2022-11-22 16:00:51 -07:00
mars 1453ab0c46 SAO UI music player sets position from slider 2022-11-21 21:06:53 -07:00
mars f37ec4251d Add interactive Slider functionality 2022-11-21 21:06:25 -07:00
mars 238f4f878d Implement music player SetPosition message 2022-11-21 21:06:03 -07:00
mars 2b058c7202 Use slider widget for position in music player 2022-11-21 20:25:41 -07:00
mars 0fe8139e02 Add initial Slider widget 2022-11-21 20:25:29 -07:00
mars 415d9e7845 Make SAO UI base colors more opaque 2022-11-20 12:18:47 -07:00
mars e0ec4190fc Enable vsync and MSAAx4 on Magpie windows 2022-11-20 12:16:55 -07:00
mars 4a78743c40 SAO UI placeholder album art uses theme's overlay color instead of magenta 2022-11-20 12:08:02 -07:00
mars 80ba46a0d4 Merge pull request 'Script-to-host messages' (#45) from script-to-host-messages into main
Reviewed-on: #45
2022-11-20 19:06:03 +00:00
mars a73869aae5 Music player responds to panel messages 2022-11-20 12:04:18 -07:00
mars b005176bcf Switch to async UnixStream in music player 2022-11-20 11:39:31 -07:00
mars 2d242520c1 Add send_panel_json_async() 2022-11-20 11:39:12 -07:00
mars ac5d6475f1 Add Messenger::send_async() 2022-11-20 11:24:58 -07:00
mars c244834004 Better Messenger recv-related docs and names 2022-11-20 11:15:50 -07:00
mars 763c55d3e4 Add async feature + Messenger::recv_async() 2022-11-20 11:08:23 -07:00
mars 6c2bfc0ea5 Split Messenger into MessageQueue 2022-11-20 10:48:49 -07:00
mars f40d251ca4 Merge branch 'main' into script-to-host-messages 2022-11-20 00:38:15 -07:00
mars d5b168ebc9 Music player resets position on track change 2022-11-20 00:37:11 -07:00
mars d99c64f33e Track position 2022-11-20 00:33:05 -07:00
mars 452cb9c708 Music player reads mpris:length instead of xesam:length 2022-11-20 00:28:49 -07:00
mars 9d4d5cca91 Merge branch 'main' into script-to-host-messages 2022-11-20 00:15:36 -07:00
mars 2144a2ab3d Merge pull request 'zbus music player' (#44) from zbus-music-player into main
Reviewed-on: #44
2022-11-20 07:07:42 +00:00
mars 2df2bd3f8f Use new length position in SAO UI 2022-11-20 00:00:43 -07:00
mars 1fe366ce9f Move progress length to TrackInfo 2022-11-20 00:00:21 -07:00
mars 9eb2c1c431 Remove old mpris comments 2022-11-19 23:56:09 -07:00
mars 13a9735bdc Update script playback status + metadata 2022-11-19 23:51:34 -07:00
mars a65164d70b Music player lifetime loop 2022-11-19 23:33:06 -07:00
mars 7e4900c59b Parse metadata 2022-11-19 23:11:17 -07:00
mars 15f9c7ea99 Listen for multiple property changes 2022-11-19 22:57:33 -07:00
mars a2432f77d2 Find active player and listen for playback status changes 2022-11-19 22:16:53 -07:00
mars 55859ab5c0 Super WIP zbus-based music player 2022-11-19 21:42:16 -07:00
mars 756238feab Initial notifications crate 2022-11-19 20:45:35 -07:00
mars d7323323f8 Add SAO UI Palette widget 2022-11-19 15:11:09 -07:00
mars 59c34d6aca Clean up SAO UI color definitions 2022-11-19 13:09:53 -07:00
mars 046aede0fa Add Arctica SAO UI theme 2022-11-19 13:06:36 -07:00
mars 4b126ed2cf Magpie window transparency + remove window decorations 2022-11-18 17:15:29 -07:00
mars e35ee1be4c Better SAO UI base transparency 2022-11-18 17:00:47 -07:00
mars d7d4ed9519 Merge pull request 'Upgrade SAO theming' (#43) from upgrade-sao-theming into main
Reviewed-on: #43
2022-11-18 23:53:16 +00:00
mars e924a17073 Disable Magpie glium gamma-correction 2022-11-18 16:48:17 -07:00
mars 25532f4f9e Use theme in MusicPlayerWidget 2022-11-18 14:30:26 -07:00
mars ef6a81e142 Add Rose Pine Moon color palette 2022-11-18 14:13:50 -07:00
mars bc8032aaaf Use THEME in ScrollBarStyle 2022-11-18 14:01:31 -07:00
mars e8c9088327 Use THEME in RectButtonStyle 2022-11-18 14:01:20 -07:00
mars 59a673128c Add base_hover and base_active colors 2022-11-18 14:00:16 -07:00
mars ce7d641795 Use theme in TabMenu 2022-11-18 13:42:18 -07:00
mars 3f7ebeaf7e Use theme in main menu 2022-11-18 13:42:06 -07:00
mars a477c3c385 Add panel background color edit in sandbox 2022-11-18 13:33:21 -07:00
mars 85e1fbd6b6 Use terminal colors for dialog buttons 2022-11-18 13:15:34 -07:00
mars 6db0c72f4d Add terminal colors to style 2022-11-18 13:15:22 -07:00
mars 1ac70e653b Use THEME in dialog style defaults 2022-11-18 13:08:28 -07:00
mars 948dfa77c4 Add style and THEME to widget prelude 2022-11-18 13:08:17 -07:00
mars 2f5d25a3f4 Add Metrics and Theme consts 2022-11-18 13:08:01 -07:00
mars 218e2fde74 Add wip-dialog protocol to display ConfirmationDialogPanel 2022-11-18 13:07:37 -07:00
mars f33100cfa8 Add SAO UI style module 2022-11-18 12:43:23 -07:00
mars 2aadd0d57a Make Color alpha functions const 2022-11-18 12:43:10 -07:00
mars a809af7c64 Replace dedicated MagpieClient with methods on ClientMessenger 2022-11-17 15:03:12 -07:00
mars d020caa477 Make Messenger transport private 2022-11-17 14:46:48 -07:00
mars 7de22f9bd4 Merge pull request 'Check for license information parity' (#36) from emma/canary-rs:main into main
Reviewed-on: #36
2022-11-17 06:30:35 +00:00
mars 50410c7f4f Merge branch 'main' into main 2022-11-17 06:30:27 +00:00
mars f81f55485b Forward script messages from IPC to client 2022-11-16 23:04:42 -07:00
mars e1ebfab247 Close Messenger connection on reset 2022-11-16 22:52:02 -07:00
mars cf95b52f7e Print error instead of panicking on unrecognized IPC even ttoken 2022-11-16 22:29:02 -07:00
mars efae627263 Forward script messages from window to IPC 2022-11-16 22:20:30 -07:00
mars 808d62421c Implement window-to-IPC messaging 2022-11-16 22:17:17 -07:00
mars 7d751f27c9 Add RecvMessage to Magpie protocol 2022-11-16 22:02:03 -07:00
mars 32cf5e3359 SAO UI music player sends Prev/Play/Next events on button presses 2022-11-16 21:11:34 -07:00
mars 0cc55b80a8 Add panel_send_message script API 2022-11-16 21:10:14 -07:00
mars 14540db59c Add panel_send_message and PanelAbi 2022-11-16 21:08:17 -07:00
mars a84f11ec4a Merge pull request 'SAO music player controller' (#42) from sao-music-player into main
Reviewed-on: #42
2022-11-17 02:41:23 +00:00
mars 3ddda5f0c1 SAO UI uses MusicPlayerPanel when the protocol is selected 2022-11-16 19:37:28 -07:00
mars 317541486c Merge branch 'main' into sao-music-player 2022-11-16 19:31:42 -07:00
mars f58e0d686d Merge pull request 'Protocol names' (#41) from protocol-names into main
Reviewed-on: #41
2022-11-17 02:29:38 +00:00
mars 53eccd7411 Update scripts to use new protocol names 2022-11-16 19:27:54 -07:00
mars f8f59cb265 Refactor script crate to use bind_panel function w/ protocol name instead of trait 2022-11-16 19:27:27 -07:00
mars 4e9b01810a Add protocol name to sandbox 2022-11-16 19:19:41 -07:00
mars 0b8ce299b5 Add protocol name to music player 2022-11-15 23:52:11 -07:00
mars f4a366a215 Add protocol name support to Magpie 2022-11-15 23:51:52 -07:00
Emma Tebibyte 3ec3dd97a5 clipped absolute paths 2022-11-16 01:25:08 -05:00
Emma Tebibyte 5670793f90 checks for license information parity between Cargo.toml and source files 2022-11-16 00:50:39 -05:00
mars cedd5503e6 Use &str instead of String for protocol names 2022-11-15 21:57:52 -07:00
mars 7c738c24ec Add protocol names to host-side panel binding 2022-11-15 21:48:07 -07:00
mars e5f9985bb9 Music player sends ProgressChanged events on track change 2022-11-15 21:41:13 -07:00
mars 71a4eb2cf0 Use progress info in position/duration labels 2022-11-15 21:37:03 -07:00
mars 7f7a88cb15 Merge pull request 'Fix sandbox coords' (#39) from fix-sandbox-coords into main
Reviewed-on: #39
2022-11-16 00:12:59 +00:00
mars a925d6b94d Fix use of PX_PER_MM 2022-11-15 17:12:04 -07:00
mars f192983319 Remove resize debug message 2022-11-15 17:03:54 -07:00
mars 7b2a110c12 Resizeable sandbox panels 2022-11-15 17:02:15 -07:00
mars a648ef360e Fix vertex and cursor sandbox coords 2022-11-15 16:51:41 -07:00
mars 58af1e341f Merge pull request 'License housekeeping' (#38) from better-licensing into main
Reviewed-on: #38
2022-11-15 00:35:38 +00:00
mars da0b2f6eb1 Add AGPL licensing to music player app and script 2022-11-14 17:34:27 -07:00
mars d4d8cab9ad Fix typos in Canary license headers 2022-11-14 17:28:38 -07:00
mars fe6d9ac1a7 Add missing license headers to Magpie sources 2022-11-14 17:22:27 -07:00
mars 2b3baf2198 Remove empty SAO UI Rust modules 2022-11-14 17:21:58 -07:00
mars 3271c35e41 Add missing license headers to SAO UI sources 2022-11-14 17:20:27 -07:00
Emma Tebibyte 42abb1791d pattern, not file 2022-11-12 03:17:40 -05:00
Emma Tebibyte 31d8bb1fd6 portability B) 2022-11-12 03:11:20 -05:00
Emma Tebibyte 359b6897a7 file, not path 2022-11-12 01:29:09 -05:00
Emma Tebibyte 1bdfb798ea usage 2022-11-12 01:22:37 -05:00
Emma Tebibyte 1f8d301103 exit code 2022-11-11 23:59:40 -05:00
Emma Tebibyte 77f5d5c7ce added dependency check for rg 2022-11-11 23:56:48 -05:00
Iris Pupo 05d68ab3c1 enabled wasmtime built in caching 2022-11-08 17:40:50 -05:00
mars 066430ccba Un-trait ScriptAbi + runtime shared FontStore (closes #15) 2022-11-07 12:37:47 -07:00
mars 4b05f55725 Shrink dialog footer height 2022-11-07 12:17:22 -07:00
mars fe9ea34a23 Add location and duration labels 2022-11-07 12:16:35 -07:00
mars 8c9f821835 Add previous, play/pause, and next buttons 2022-11-06 19:01:13 -07:00
mars 4a091bd206 Label text update methods 2022-11-06 19:00:56 -07:00
mars 1e43b0a2c4 WIP SAO UI music player 2022-11-06 18:27:43 -07:00
mars 87d70ee6d1 Magpie supports Hover and Drag cursor events 2022-11-06 16:27:09 -07:00
mars 75fb80adf3 Fix coordinate system for SAO UI (closes #28) 2022-11-06 16:26:22 -07:00
mars e39d16a516 Dialog uses new coords 2022-11-06 14:59:38 -07:00
mars 69318a02e5 Fix rounded corner drawing 2022-11-05 16:08:07 -06:00
mars ab50a8e130 Fix use of Rect in SAO UI script 2022-11-05 15:03:30 -06:00
mars 75e0aca668 Add missing SAO UI on_resize() impls 2022-11-05 15:01:06 -06:00
mars eb32163b24 Fix Rect coordinate space 2022-11-05 15:00:51 -06:00
mars 681b884b74 Add Color::lerp() 2022-11-05 14:25:59 -06:00
78 changed files with 2934 additions and 925 deletions

View File

@ -2,8 +2,10 @@
members = [
"apps/magpie",
"apps/music-player",
"apps/notifications",
"apps/sandbox",
"crates/script",
"crates/textwrap",
"scripts/music-player",
"scripts/sao-ui",
]
@ -19,11 +21,13 @@ allsorts = "0.10"
anyhow = "1"
bytemuck = "1"
canary-script = { path = "crates/script" }
log = "0.4"
lyon = "1"
ouroboros = "^0.15"
parking_lot = "0.12"
prehash = "0.3.3"
slab = "0.4"
wasmtime = "0.38"
wasmtime = "3"
[dependencies.font-kit]
version = "*"

View File

@ -1,13 +1,39 @@
# Canary
Canary is a post-structuralist graphical user interface (GUI) framework that
uses standardized message-passing protocols to represent UI state instead of a
typical DOM-based GUI workflow.
Canary scripts (executed as WebAssembly) implement all of the rendering and
layout of the GUIs, so the host has little to no involvement in their
appearance. This allows virtually unlimited customization of Canary GUIs, as
scripts can be reloaded by applications with no impact on application behavior.
Canary's development has been documented on Tebibyte Media's blog:
https://tebibyte.media/blog/project/canary/
# Screenshots
<div align="center">
<figure>
<img src="./resources/sandbox-screenshot.jpg"/>
<figcaption>A screenshot of the Canary sandbox and Sword Art Online script.</figcaption>
</figure>
<figure>
<img src="./resources/music-player-screenshot.jpg"/>
<figcaption>A screenshot of the desktop music player controller using the Sword Art Online script.</figcaption>
</figure>
</div>
# Using `canary-rs`
[`canary-rs`](https://git.tebibyte.media/canary/canary-rs) is the reference
implementation for Canary. It is written in Rust, and is licensed under the
LGPLv3.
This repository (`canary-rs`) is the reference implementation for Canary. It is
written in Rust, and is licensed under the LGPLv3.
`canary-rs` is the central hub for Canary's development. It includes host-side
Rust code, helper crates for Canary hosts, wrapper crates for scripts
authored in Rust, and even the source code for the documentation that you're
currently reading.
authored in Rust, and the documentation that you're currently reading.
`canary-rs` provides a graphical "sandbox" that embeds the Canary runtime
into a lightweight graphical app. It has two purposes: first, to give
@ -16,13 +42,13 @@ benchmark, and experiment with their scripts, and second, to give Canary
embedders a live, functioning example of how Canary can be integrated into their
applications.
# Running the `canary-rs` sandbox
## Running the `canary-rs` sandbox
The sandbox requires a Canary script to run. If you don't already have one,
you can follow [these instructions](optional-building-the-sword-art-online-demonstration-ui-script)
to build the example script provided by `canary-rs`.
## Building the sandbox
### Building the sandbox
To build the sandbox from source, first make sure that you have
[installed the standard Rust toolchain](https://www.rustlang.org/tools/install),
@ -46,6 +72,8 @@ Now, the sandbox can be ran with a script:
$ cargo run --release -p canary_sandbox -- <path-to-script>
```
## Running Magpie
## (Optional) Building the Sword Art Online demonstration UI script
`canary-rs` provides an example of a fully-functioning script which, optionally,
@ -71,7 +99,7 @@ Now it can be run using the sandbox:
$ cargo run --release -p canary_sandbox -- target/wasm32-unknown-unknown/release/sao_ui_rs.wasm
```
# Using `canary-rs` as a Rust library
## Using `canary-rs` as a Rust library
***WARNING***: `canary-rs` is still in alpha development so both its API and its
version number are unstable. It is not recommended to use it in your own

View File

@ -2,6 +2,7 @@
name = "canary-magpie"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[[bin]]
name = "magpie"
@ -12,7 +13,10 @@ required-features = ["service"]
anyhow = { version = "1", optional = true }
byteorder = "1.4"
canary = { path = "../..", optional = true }
env_logger = { version = "0.10", optional = true }
futures-util = { version = "0.3", optional = true, features = ["io"] }
glium = { version = "0.32", optional = true}
log = "0.4"
mio = { version = "0.8", features = ["net", "os-poll"], optional = true }
mio-signals = { version = "0.2", optional = true }
parking_lot = { version = "0.12", optional = true}
@ -21,4 +25,5 @@ serde_json = "1"
slab = { version = "0.4", optional = true}
[features]
service = ["dep:anyhow", "dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
async = ["dep:futures-util"]
service = ["dep:anyhow", "dep:canary", "dep:env_logger", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]

View File

@ -1,41 +0,0 @@
use serde::Serialize;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::protocol::{ClientMessenger, MagpieServerMsg, PanelId, SendMessage, MAGPIE_SOCK};
/// A client to a Magpie server.
pub struct MagpieClient {
pub messenger: ClientMessenger<UnixStream>,
}
impl MagpieClient {
pub fn new() -> std::io::Result<Self> {
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(MAGPIE_SOCK);
let socket = UnixStream::connect(sock_path)?;
Ok(Self {
messenger: ClientMessenger::new(socket),
})
}
pub fn send(&mut self, msg: &MagpieServerMsg) {
if let Err(err) = self.messenger.send(msg) {
eprintln!("Message send error: {:?}", err);
}
}
pub fn send_json_message<T: Serialize>(&mut self, id: PanelId, msg: &T) {
let msg = serde_json::to_string(msg).unwrap();
eprintln!("Sending message: {}", msg);
let msg = SendMessage {
id,
msg: msg.into_bytes(),
};
self.send(&MagpieServerMsg::SendMessage(msg));
}
}

View File

@ -1,4 +1,6 @@
pub mod client;
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod protocol;
#[cfg(feature = "service")]

View File

@ -1,3 +1,6 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use glium::glutin::event_loop::EventLoopBuilder;
use canary_magpie::service::*;
@ -5,6 +8,12 @@ use ipc::Ipc;
use window::{WindowMessage, WindowStore};
fn main() -> std::io::Result<()> {
env_logger::Builder::new()
.filter(None, log::LevelFilter::Info) // By default logs all info messages.
.parse_default_env()
.init();
log::info!("Initializing Magpie...");
let event_loop = EventLoopBuilder::<WindowMessage>::with_user_event().build();
let window_sender = event_loop.create_proxy();
let (ipc, ipc_sender) = Ipc::new(window_sender)?;

View File

@ -1,7 +1,10 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::VecDeque;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use std::path::{PathBuf, Path};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@ -21,7 +24,15 @@ pub type PanelId = u32;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CreatePanel {
pub id: PanelId,
pub protocol: String,
pub script: PathBuf,
pub init_msg: Vec<u8>,
}
/// Closes a Magpie panel with a given ID.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ClosePanel {
pub id: PanelId,
}
/// Sends a panel a message.
@ -36,74 +47,61 @@ pub struct SendMessage {
#[serde(tag = "kind")]
pub enum MagpieServerMsg {
CreatePanel(CreatePanel),
ClosePanel(ClosePanel),
SendMessage(SendMessage),
}
/// A message sent from a script's panel to a client.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RecvMessage {
pub id: PanelId,
pub msg: Vec<u8>,
}
/// A message sent from the Magpie server to a client.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "kind")]
pub enum MagpieClientMsg {}
pub enum MagpieClientMsg {
RecvMessage(RecvMessage),
}
/// A [Messenger] specialized for Magpie clients.
pub type ClientMessenger<T> = Messenger<T, MagpieClientMsg, MagpieServerMsg>;
impl<T: Write> ClientMessenger<T> {
pub fn send_panel_json<O: Serialize>(&mut self, id: PanelId, msg: &O) {
let msg = serde_json::to_string(msg).unwrap();
let _ = self.send(&MagpieServerMsg::SendMessage(SendMessage {
id,
msg: msg.into_bytes(),
}));
}
}
/// A [Messenger] specialized for Magpie servers.
pub type ServerMessenger<T> = Messenger<T, MagpieServerMsg, MagpieClientMsg>;
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
pub struct Messenger<T, I, O> {
pub transport: T,
/// Piecewise packet assembler for [Messenger].
pub struct MessageQueue<I> {
expected_len: Option<usize>,
received_buf: VecDeque<u8>,
received_queue: VecDeque<I>,
closed: bool,
_output: PhantomData<O>,
}
impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
pub fn new(transport: T) -> Self {
impl<I> Default for MessageQueue<I> {
fn default() -> Self {
Self {
transport,
expected_len: None,
received_buf: Default::default(),
received_queue: Default::default(),
closed: false,
_output: PhantomData,
}
}
}
pub fn is_closed(&self) -> bool {
self.closed
}
pub fn send(&mut self, msg: &O) -> std::io::Result<()> {
use byteorder::{LittleEndian, WriteBytesExt};
let payload = serde_json::to_vec(msg).unwrap();
let len = payload.len() as u32;
self.transport.write_u32::<LittleEndian>(len)?;
self.transport.write_all(&payload)?;
self.transport.flush()?;
Ok(())
}
/// Receives all pending messages and queues them for [recv].
pub fn flush_recv(&mut self) -> std::io::Result<()> {
let mut buf = [0u8; 1024];
loop {
match self.transport.read(&mut buf) {
Ok(0) => {
self.closed = true;
break;
}
Ok(n) => {
self.received_buf.write(&buf[..n])?;
}
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
impl<I: DeserializeOwned> MessageQueue<I> {
pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> {
self.received_buf.write_all(data)?;
loop {
if let Some(expected_len) = self.expected_len {
@ -135,8 +133,146 @@ impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
Ok(())
}
/// Tries to receive a single input packet.
pub fn recv(&mut self) -> Option<I> {
self.received_queue.pop_back()
}
}
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
pub struct Messenger<T, I, O> {
transport: T,
queue: MessageQueue<I>,
closed: bool,
_output: PhantomData<O>,
}
impl<T, I, O> Messenger<T, I, O> {
pub fn new(transport: T) -> Self {
Self {
transport,
queue: Default::default(),
closed: false,
_output: PhantomData,
}
}
pub fn is_closed(&self) -> bool {
self.closed
}
/// Destroys this messenger and returns the inner transport.
pub fn into_transport(self) -> T {
self.transport
}
}
impl<T: Write, I, O: Serialize> Messenger<T, I, O> {
pub fn send(&mut self, msg: &O) -> std::io::Result<()> {
use byteorder::{LittleEndian, WriteBytesExt};
let payload = serde_json::to_vec(msg).unwrap();
let len = payload.len() as u32;
self.transport.write_u32::<LittleEndian>(len)?;
self.transport.write_all(&payload)?;
self.transport.flush()?;
Ok(())
}
}
impl<T: Read, I: DeserializeOwned, O> Messenger<T, I, O> {
/// Synchronously receives all pending messages and queues them for [recv].
///
/// This function only works if the transport is in non-blocking mode.
/// Otherwise, this may block while waiting for more data, even if the
/// data it receives does not add up to a full message.
pub fn flush_recv(&mut self) -> std::io::Result<()> {
let mut buf = [0u8; 1024];
loop {
match self.transport.read(&mut buf) {
Ok(0) => {
self.closed = true;
break;
}
Err(ref err) if err.kind() == std::io::ErrorKind::ConnectionReset => {
self.closed = true;
break;
}
Ok(n) => {
self.queue.on_data(&buf[..n])?;
}
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
Ok(())
}
/// Tries to receive a single input packet.
///
/// For messages to be received here, [flush_recv] must be called to
/// continuously read pending data from the transport.
pub fn try_recv(&mut self) -> Option<I> {
self.queue.recv()
}
}
/// Acquires the path to the Magpie socket.
///
/// Currently only joins XDG_RUNTIME_DIR with [MAGPIE_SOCK].
pub fn find_socket() -> PathBuf {
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(MAGPIE_SOCK);
sock_path
}
#[cfg(feature = "async")]
mod async_messages {
use super::*;
use futures_util::{AsyncReadExt, AsyncWriteExt};
use std::marker::Unpin;
impl<T: AsyncWriteExt + Unpin> ClientMessenger<T> {
pub async fn send_panel_json_async<O: Serialize>(&mut self, id: PanelId, msg: &O) {
let msg = serde_json::to_string(msg).unwrap();
let _ = self
.send_async(&MagpieServerMsg::SendMessage(SendMessage {
id,
msg: msg.into_bytes(),
}))
.await;
}
}
impl<T: AsyncWriteExt + Unpin, I, O: Serialize> Messenger<T, I, O> {
pub async fn send_async(&mut self, msg: &O) -> std::io::Result<()> {
use byteorder::{LittleEndian, WriteBytesExt};
let payload = serde_json::to_vec(msg).unwrap();
let len = payload.len() as u32;
let mut msg = Vec::with_capacity(4 + payload.len());
msg.write_u32::<LittleEndian>(len)?;
msg.extend_from_slice(&payload);
self.transport.write_all(&msg).await?;
self.transport.flush().await?;
Ok(())
}
}
impl<T: AsyncReadExt + Unpin, I: DeserializeOwned, O> Messenger<T, I, O> {
pub async fn recv(&mut self) -> std::io::Result<I> {
let mut buf = [0u8; 1024];
loop {
if let Some(msg) = self.queue.recv() {
return Ok(msg);
}
let num = self.transport.read(&mut buf).await?;
self.queue.on_data(&buf[..num])?;
}
}
}
}

View File

@ -1,5 +1,8 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary::{DrawCommand, Vec2, PX_PER_MM};
use glium::Surface;
use glium::{program::ProgramCreationInput, Surface};
#[derive(Copy, Clone)]
pub struct Vertex {
@ -56,9 +59,21 @@ pub struct Graphics {
impl Graphics {
pub fn new(display: glium::Display) -> Self {
let program =
glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None)
.unwrap();
let program = glium::Program::new(
&display,
ProgramCreationInput::SourceCode {
vertex_shader: VERTEX_SHADER_SRC,
tessellation_control_shader: None,
tessellation_evaluation_shader: None,
geometry_shader: None,
fragment_shader: FRAGMENT_SHADER_SRC,
transform_feedback_varyings: None,
outputs_srgb: true, // don't automatically apply gamma correction
uses_point_size: false,
},
)
.unwrap();
Self { display, program }
}
@ -102,7 +117,7 @@ impl Graphics {
};
let mut target = self.display.draw();
target.clear_color(0.0, 0.0, 0.0, 1.0);
target.clear_color(0.0, 0.0, 0.0, 0.0);
target
.draw(
&vertex_buffer,

View File

@ -1,3 +1,6 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
@ -11,18 +14,29 @@ use mio_signals::{Signal, Signals};
use parking_lot::RwLock;
use slab::Slab;
use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger};
use crate::protocol::*;
use crate::service::window::{WindowMessage, WindowMessageSender};
const SOCK_NAME: &str = "magpie.sock";
pub enum IpcMessage {}
#[derive(Debug)]
pub enum IpcMessage {
PanelMessage { window: usize, message: Vec<u8> },
}
#[derive(Clone)]
pub struct IpcMessageSender {
waker: Waker,
waker: Arc<Waker>,
sender: Sender<IpcMessage>,
}
impl IpcMessageSender {
pub fn send(&self, msg: IpcMessage) {
let _ = self.sender.send(msg);
let _ = self.waker.wake();
}
}
/// Wraps [mio::net::UnixListener] with automatic file deletion on drop.
pub struct Listener {
pub uds: UnixListener,
@ -33,7 +47,7 @@ impl Drop for Listener {
fn drop(&mut self) {
match std::fs::remove_file(&self.path) {
Ok(_) => {}
Err(e) => eprintln!("Could not delete UnixListener {:?}", e),
Err(e) => log::error!("Could not delete UnixListener {:?}", e),
}
}
}
@ -52,8 +66,38 @@ impl DerefMut for Listener {
}
}
impl Listener {
fn new() -> std::io::Result<Self> {
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(SOCK_NAME);
use std::io::{Error, ErrorKind};
match UnixStream::connect(&sock_path) {
Ok(_) => {
log::warn!("Socket is already in use. Another instance of Magpie may be running.");
let kind = ErrorKind::AddrInUse;
let error = Error::new(kind, "Socket is already in use.");
return Err(error);
}
Err(ref err) if err.kind() == ErrorKind::ConnectionRefused => {
log::warn!("Found leftover socket; removing.");
std::fs::remove_file(&sock_path)?;
}
Err(ref err) if err.kind() == ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
log::info!("Making socket at: {:?}", sock_path);
let uds = UnixListener::bind(&sock_path)?;
let path = sock_path.to_path_buf();
Ok(Self { uds, path })
}
}
pub struct IpcData {
poll: Poll,
window_to_client_panel: HashMap<usize, (usize, PanelId)>,
next_window_id: usize,
}
@ -73,40 +117,46 @@ pub struct Client {
id_to_window: HashMap<u32, usize>,
}
impl Drop for Client {
fn drop(&mut self) {
println!("Client #{} disconnected", self.token.0);
let data = self.data.write();
let _ = data
.poll
.registry()
.deregister(&mut self.messenger.transport);
for (_id, window) in self.id_to_window.drain() {
let msg = WindowMessage::CloseWindow { id: window };
let _ = self.window_sender.send_event(msg);
}
}
}
impl Client {
pub fn on_readable(&mut self) -> std::io::Result<bool> {
self.messenger.flush_recv()?;
if let Err(err) = self.messenger.flush_recv() {
log::error!("flush_recv() error: {:?}", err);
}
while let Some(msg) = self.messenger.recv() {
println!("Client #{}: {:?}", self.token.0, msg);
while let Some(msg) = self.messenger.try_recv() {
log::debug!("Client #{}: {:?}", self.token.0, msg);
match msg {
MagpieServerMsg::CreatePanel(CreatePanel { id, script }) => {
let window = self.data.write().new_window_id();
MagpieServerMsg::CreatePanel(CreatePanel {
id,
protocol,
script,
init_msg,
}) => {
let mut data = self.data.write();
let window = data.new_window_id();
data.window_to_client_panel
.insert(window, (self.token.0, id));
if let Some(old_id) = self.id_to_window.insert(id, window) {
let msg = WindowMessage::CloseWindow { id: old_id };
let _ = self.window_sender.send_event(msg);
}
let msg = WindowMessage::OpenWindow { id: window, script };
let msg = WindowMessage::OpenWindow {
id: window,
protocol,
script,
init_msg,
};
let _ = self.window_sender.send_event(msg);
}
MagpieServerMsg::ClosePanel(ClosePanel { id }) => {
if let Some(id) = self.id_to_window.get(&id).copied() {
let msg = WindowMessage::CloseWindow { id };
let _ = self.window_sender.send_event(msg);
}
}
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
if let Some(id) = self.id_to_window.get(&id).cloned() {
let msg = WindowMessage::SendMessage { id, msg };
@ -118,13 +168,26 @@ impl Client {
Ok(self.messenger.is_closed())
}
pub fn disconnect(mut self) {
log::info!("Client #{} disconnected", self.token.0);
let mut transport = self.messenger.into_transport();
let mut data = self.data.write();
let _ = data.poll.registry().deregister(&mut transport);
for (_id, window) in self.id_to_window.drain() {
let msg = WindowMessage::CloseWindow { id: window };
let _ = self.window_sender.send_event(msg);
data.window_to_client_panel.remove(&window);
}
}
}
pub struct Ipc {
pub data: Arc<RwLock<IpcData>>,
pub window_sender: WindowMessageSender,
pub message_recv: Receiver<IpcMessage>,
pub events: Events,
pub quit: bool,
pub listener: Listener,
pub signals: Signals,
@ -136,19 +199,10 @@ pub struct Ipc {
impl Ipc {
pub fn new(window_sender: WindowMessageSender) -> std::io::Result<(Self, IpcMessageSender)> {
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(SOCK_NAME);
eprintln!("Making socket at: {:?}", sock_path);
let mut listener = Listener {
uds: UnixListener::bind(&sock_path)?,
path: sock_path.to_path_buf(),
};
let mut listener = Listener::new()?;
let mut signals = Signals::new(Signal::Interrupt | Signal::Quit)?;
let events = Events::with_capacity(128);
let poll = Poll::new()?;
let listener_token = Token(usize::MAX);
let signals_token = Token(listener_token.0 - 1);
@ -162,12 +216,13 @@ impl Ipc {
let (sender, message_recv) = channel();
let sender = IpcMessageSender {
waker: Waker::new(registry, message_recv_token)?,
waker: Arc::new(Waker::new(registry, message_recv_token)?),
sender,
};
let data = IpcData {
poll,
window_to_client_panel: HashMap::new(),
next_window_id: 0,
};
@ -175,7 +230,6 @@ impl Ipc {
data: Arc::new(RwLock::new(data)),
window_sender,
message_recv,
events,
quit: false,
listener,
signals,
@ -188,18 +242,38 @@ impl Ipc {
Ok((ipc, sender))
}
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
self.data.write().poll.poll(&mut self.events, timeout)?;
pub fn on_message(&mut self, msg: IpcMessage) -> std::io::Result<()> {
match msg {
IpcMessage::PanelMessage { window, message } => {
let data = self.data.read();
let (client, panel) = *data.window_to_client_panel.get(&window).unwrap();
let client = self.clients.get_mut(client).unwrap();
let reply = RecvMessage {
id: panel,
msg: message,
};
client
.messenger
.send(&MagpieClientMsg::RecvMessage(reply))?;
}
}
for event in self.events.iter() {
Ok(())
}
pub fn poll(&mut self, events: &mut Events, timeout: Option<Duration>) -> std::io::Result<()> {
self.data.write().poll.poll(events, timeout)?;
for event in events.iter() {
if event.token() == self.listener_token {
loop {
match self.listener.accept() {
Ok((mut connection, address)) => {
let token = Token(self.clients.vacant_key());
println!(
log::info!(
"Accepting connection (Client #{}) from {:?}",
token.0, address
token.0,
address
);
let interest = Interest::READABLE;
@ -223,17 +297,21 @@ impl Ipc {
}
} else if event.token() == self.signals_token {
while let Some(received) = self.signals.receive()? {
eprintln!("Received {:?} signal; exiting...", received);
log::info!("Received {:?} signal; exiting...", received);
let _ = self.window_sender.send_event(WindowMessage::Quit);
self.quit = true;
}
} else if event.token() == self.message_recv_token {
while let Ok(received) = self.message_recv.try_recv() {
self.on_message(received)?;
}
} else if let Some(client) = self.clients.get_mut(event.token().0) {
let disconnected = client.on_readable()?;
if disconnected {
self.clients.remove(event.token().0);
self.clients.remove(event.token().0).disconnect();
}
} else {
panic!("Unrecognized event token: {:?}", event);
log::error!("Unrecognized event token: {:?}", event);
}
}
@ -241,12 +319,13 @@ impl Ipc {
}
pub fn run(mut self) {
let mut events = Events::with_capacity(128);
while !self.quit {
let wait = Duration::from_millis(100);
match self.poll(Some(wait)) {
match self.poll(&mut events, Some(wait)) {
Ok(_) => {}
Err(e) => {
eprintln!("IPC poll error: {:?}", e);
log::error!("IPC poll error: {:?}", e);
}
}
}

View File

@ -1,3 +1,6 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod gl;
pub mod ipc;
pub mod window;

View File

@ -1,3 +1,6 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Instant;
@ -14,36 +17,58 @@ use crate::service::ipc::{IpcMessage, IpcMessageSender};
#[derive(Clone, Debug)]
pub enum WindowMessage {
OpenWindow { id: usize, script: PathBuf },
CloseWindow { id: usize },
OpenWindow {
id: usize,
protocol: String,
script: PathBuf,
init_msg: Vec<u8>,
},
CloseWindow {
id: usize,
},
Quit,
SendMessage { id: usize, msg: Vec<u8> },
SendMessage {
id: usize,
msg: Vec<u8>,
},
}
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
pub struct Window {
pub ipc_sender: IpcMessageSender,
pub ipc_id: usize,
pub graphics: Graphics,
pub panel: Panel,
pub last_update: Instant,
pub cursor_pos: Vec2,
pub cursor_down: bool,
}
impl Window {
pub fn new(
ipc_sender: IpcMessageSender,
ipc_id: usize,
panel: Panel,
event_loop: &EventLoopWindowTarget<WindowMessage>,
) -> Result<Self, DisplayCreationError> {
let wb = glutin::window::WindowBuilder::new();
let cb = glutin::ContextBuilder::new();
let wb = glutin::window::WindowBuilder::new()
.with_transparent(true)
.with_decorations(false);
let cb = glutin::ContextBuilder::new()
.with_vsync(true)
.with_multisampling(4);
let display = glium::Display::new(wb, cb, &event_loop)?;
let graphics = Graphics::new(display);
let last_update = Instant::now();
Ok(Self {
ipc_sender,
ipc_id,
graphics,
panel,
last_update,
cursor_pos: Vec2::ZERO,
cursor_down: false,
})
}
@ -55,24 +80,38 @@ impl Window {
self.graphics.display.gl_window().window().request_redraw();
}
/// Receives all messages from the script and forwards them to IPC.
pub fn recv_messages(&mut self) {
for message in self.panel.recv_messages() {
self.ipc_sender.send(IpcMessage::PanelMessage {
window: self.ipc_id,
message,
});
}
}
pub fn update(&mut self) {
let now = Instant::now();
let dt = now.duration_since(self.last_update).as_secs_f32();
self.panel.update(dt);
self.last_update = now;
self.recv_messages();
}
pub fn draw(&mut self) {
let commands = self.panel.draw();
self.graphics.draw(&commands);
self.recv_messages();
}
pub fn send_message(&mut self, msg: Vec<u8>) {
self.panel.on_message(msg);
self.recv_messages();
}
pub fn resize(&mut self, new_size: Vec2) {
self.panel.on_resize(new_size);
self.recv_messages();
}
pub fn on_event(&mut self, event: WindowEvent) {
@ -85,6 +124,15 @@ impl Window {
let x = position.x as f32 * PX_PER_MM;
let y = position.y as f32 * PX_PER_MM;
self.cursor_pos = Vec2::new(x, y);
let event = if self.cursor_down {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
};
self.panel.on_cursor_event(event, self.cursor_pos);
self.recv_messages();
}
WindowEvent::MouseInput {
state,
@ -92,11 +140,18 @@ impl Window {
..
} => {
let event = match state {
ElementState::Pressed => CursorEventKind::Select,
ElementState::Released => CursorEventKind::Deselect,
ElementState::Pressed => {
self.cursor_down = true;
CursorEventKind::Select
}
ElementState::Released => {
self.cursor_down = false;
CursorEventKind::Deselect
}
};
self.panel.on_cursor_event(event, self.cursor_pos);
self.recv_messages();
}
_ => {}
}
@ -136,15 +191,24 @@ impl WindowStore {
message: WindowMessage,
) -> anyhow::Result<bool> {
match message {
WindowMessage::OpenWindow { id, script } => {
println!("Opening window {} with script {:?}", id, script);
WindowMessage::OpenWindow {
id,
protocol,
script,
init_msg,
} => {
log::debug!("Opening window {} with script {:?}...", id, script);
let start = std::time::Instant::now();
let module = std::fs::read(script)?;
let mut script = self.runtime.load_module(&module)?;
let panel = script.create_panel(vec![])?;
let window = Window::new(panel, &event_loop)?;
log::debug!("Instantiated window {} script in {:?}", id, start.elapsed());
let panel = script.create_panel(&protocol, init_msg)?;
log::debug!("Created window {} panel in {:?}", id, start.elapsed());
let window = Window::new(self.ipc_sender.to_owned(), id, panel, &event_loop)?;
let window_id = window.get_id();
self.windows.insert(window_id, window);
self.ipc_to_window.insert(id, window_id);
log::debug!("Opened window {} in {:?}", id, start.elapsed());
}
WindowMessage::CloseWindow { id } => {
if let Some(window_id) = self.ipc_to_window.remove(&id) {
@ -184,7 +248,7 @@ impl WindowStore {
Ok(false) => {}
Ok(true) => *control_flow = ControlFlow::Exit,
Err(err) => {
eprintln!("Error while handling message {:?}:\n{}", event, err);
log::error!("Error while handling message {:?}:\n{}", event, err);
}
},
_ => {}

View File

@ -2,6 +2,7 @@
name = "canary-music-player"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[[bin]]
name = "canary-music-player"
@ -9,10 +10,12 @@ path = "src/main.rs"
required-features = ["bin"]
[dependencies]
canary-magpie = { path = "../magpie", optional = true }
mpris = { version = "2.0.0-rc3", optional = true }
async-std = { version = "1.12", optional = true, features = ["attributes"] }
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
futures-util = { version = "0.3", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zbus = { version = "3.5", optional = true }
[features]
bin = ["dep:canary-magpie", "dep:mpris"]
bin = ["dep:async-std", "dep:canary-magpie", "dep:futures-util", "dep:zbus"]

View File

@ -1,9 +1,12 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::{Deserialize, Serialize};
pub use serde;
pub use serde_json;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum PlaybackStatus {
/// A track is currently playing.
Playing,
@ -15,7 +18,7 @@ pub enum PlaybackStatus {
Stopped,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum LoopStatus {
/// The playback will stop when there are no more tracks to play.
None,
@ -31,9 +34,6 @@ pub enum LoopStatus {
pub struct ProgressChanged {
/// Current position into the track in seconds.
pub position: f32,
/// Length of the current track in seconds.
pub length: Option<f32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -45,7 +45,7 @@ pub struct AlbumInfo {
pub artists: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TrackInfo {
/// The title of the current track.
pub title: Option<String>,
@ -55,6 +55,9 @@ pub struct TrackInfo {
/// The optional track number on the disc the album the track appears on.
pub track_number: Option<i32>,
/// Length of the track in seconds.
pub length: Option<f32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -101,6 +104,6 @@ pub enum OutMsg {
/// Sets the volume. Values are clamped to 0.0 to 1.0.
SetVolume { volume: f32 },
/// Set the current track position in seconds.
SetPosition { position: f32 },
/// Seeks the current track's position in seconds.
Seek { offset: f32 },
}

View File

@ -1,77 +1,214 @@
use canary_music_player::*;
use canary_magpie::client::MagpieClient;
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
use mpris::PlayerFinder;
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
pub struct MetadataTracker {
use canary_magpie::protocol::{
ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK,
};
use canary_music_player::*;
use async_std::os::unix::net::UnixStream;
pub type MagpieClient = ClientMessenger<UnixStream>;
pub mod mpris;
use mpris::*;
#[derive(Debug)]
pub struct Metadata {
pub album: AlbumInfo,
pub track: TrackInfo,
}
impl From<mpris::Metadata> for MetadataTracker {
fn from(metadata: mpris::Metadata) -> Self {
impl<'a> From<MetadataMap<'a>> for Metadata {
fn from(map: MetadataMap<'a>) -> Self {
let album = AlbumInfo {
title: metadata.album_name().map(ToString::to_string),
artists: metadata
.album_artists()
.unwrap_or(Vec::new())
.iter()
.map(ToString::to_string)
.collect(),
title: map
.get("xesam:album")
.and_then(|v| TryFrom::try_from(v).ok()),
artists: map
.get("xesam:albumArtist")
.cloned()
.and_then(|v| TryFrom::try_from(v).ok())
.unwrap_or(Vec::new()),
};
let track = TrackInfo {
title: metadata.title().map(ToString::to_string),
artists: metadata
.artists()
.unwrap_or(Vec::new())
.iter()
.map(ToString::to_string)
.collect(),
track_number: metadata.track_number(),
title: map
.get("xesam:title")
.and_then(|v| TryFrom::try_from(v).ok()),
artists: map
.get("xesam:artist")
.cloned()
.and_then(|v| TryFrom::try_from(v).ok())
.unwrap_or(Vec::new()),
track_number: map
.get("xesam:trackNumber")
.and_then(|v| TryFrom::try_from(v).ok()),
length: map
.get("mpris:length")
.and_then(|v| i64::try_from(v).ok())
.map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second
};
Self { album, track }
}
}
impl MetadataTracker {
pub fn new(magpie: &mut MagpieClient, metadata: mpris::Metadata) -> Self {
impl Metadata {
pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self {
let new: Self = metadata.into();
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
let msg = InMsg::AlbumChanged(new.album.clone());
magpie.send_panel_json_async(0, &msg).await;
let msg = InMsg::TrackChanged(new.track.clone());
magpie.send_panel_json_async(0, &msg).await;
new
}
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: mpris::Metadata) {
pub async fn update_diff(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap<'_>) {
let new: Self = metadata.into();
if self.album != new.album {
messenger.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
let msg = InMsg::AlbumChanged(new.album.clone());
messenger.send_panel_json_async(0, &msg).await;
}
if self.track != new.track {
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
let msg = InMsg::TrackChanged(new.track.clone());
messenger.send_panel_json_async(0, &msg).await;
let progress = ProgressChanged { position: 0.0 };
let msg = InMsg::ProgressChanged(progress);
messenger.send_panel_json_async(0, &msg).await;
}
*self = new;
}
}
fn main() {
async fn on_message(
player: &PlayerProxy<'_>,
magpie: &mut MagpieClient,
message: MagpieClientMsg,
) -> Result<(), Box<dyn std::error::Error>> {
let message = match message {
MagpieClientMsg::RecvMessage(RecvMessage { id: 0, msg }) => msg,
_ => return Ok(()),
};
let message: OutMsg = match serde_json::from_slice(&message) {
Ok(v) => v,
Err(err) => {
eprintln!("Panel message parse error: {:?}", err);
return Ok(());
}
};
match message {
OutMsg::Pause => player.pause().await?,
OutMsg::Play => player.play().await?,
OutMsg::PlayPause => player.play_pause().await?,
OutMsg::Stop => player.stop().await?,
OutMsg::Previous => player.previous().await?,
OutMsg::Next => player.next().await?,
OutMsg::Seek { offset } => {
let offset = (offset * 1_000_000.0) as i64; // Seconds to microseconds
player.seek(offset).await?;
}
_ => {}
}
Ok(())
}
async fn player_main(
player: &PlayerProxy<'_>,
magpie: &mut MagpieClient,
) -> Result<(), Box<dyn std::error::Error>> {
use futures_util::{FutureExt, StreamExt};
let mut playback_status = player.receive_playback_status_changed().await.fuse();
let mut metadata_tracker = player.receive_metadata_changed().await.fuse();
let mut position_tracker = player.receive_position_changed().await.fuse();
let mut metadata = Metadata::update_new(magpie, player.metadata().await?).await;
loop {
futures_util::select! {
msg = magpie.recv().fuse() => {
match msg {
Ok(msg) => on_message(player, magpie, msg).await?,
Err(err) => eprintln!("Magpie recv error: {:?}", err),
}
}
// TODO also update volume, shuffle status, and loop status
status = playback_status.next() => {
let status = match status {
Some(v) => v,
None => break,
};
let status = status.get().await?;
let status = match status.as_str() {
"Playing" => Some(PlaybackStatus::Playing),
"Paused" => Some(PlaybackStatus::Paused),
"Stopped" => Some(PlaybackStatus::Stopped),
_ => None,
};
if let Some(status) = status {
let msg = InMsg::PlaybackStatusChanged(status);
magpie.send_panel_json_async(0, &msg).await;
}
}
position = position_tracker.next() => {
let position = match position {
Some(v) => v,
None => break,
};
let position = position.get().await?;
let position = position as f32 / 1_000_000.0; // Microseconds to seconds
let progress = ProgressChanged { position };
let msg = InMsg::ProgressChanged(progress);
magpie.send_panel_json_async(0, &msg).await;
}
new_metadata = metadata_tracker.next() => {
let new_metadata = match new_metadata {
Some(v) => v,
None => break,
};
let new_metadata = new_metadata.get().await?;
metadata.update_diff(magpie, new_metadata).await;
}
};
}
Ok(())
}
#[async_std::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
let module_path = args
.get(1)
.expect("Please pass a path to a Canary script!")
.to_owned();
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
let mut magpie = MagpieClient::new().unwrap();
let sock_path = canary_magpie::protocol::find_socket();
let socket = UnixStream::connect(sock_path).await.unwrap();
let mut magpie = MagpieClient::new(socket);
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
let script = std::path::PathBuf::from(&module_path);
let msg = CreatePanel { id: 0, script };
let msg = CreatePanel {
id: 0,
protocol,
script,
init_msg: vec![],
};
let msg = MagpieServerMsg::CreatePanel(msg);
magpie.messenger.send(&msg).unwrap();
magpie.send_async(&msg).await.unwrap();
let dbus = zbus::Connection::session().await.unwrap();
let mut first_loop = true;
let mut connected = false;
@ -79,7 +216,7 @@ fn main() {
loop {
if !first_loop {
let wait = std::time::Duration::from_secs(1);
std::thread::sleep(wait);
async_std::task::sleep(wait).await;
}
first_loop = false;
@ -87,81 +224,36 @@ fn main() {
if connected {
println!("Disconnected from MPRIS");
let msg = InMsg::Disconnected;
magpie.send_json_message(0, &msg);
magpie.send_panel_json_async(0, &msg).await;
connected = false;
}
println!("Connecting to MPRIS...");
let player = match player_finder.find_active() {
Ok(player) => player,
Err(err) => {
eprintln!("Couldn't find player: {:?}", err);
let player = match find_player(&dbus).await {
Ok(Some(player)) => player,
Ok(None) => {
eprintln!("Couldn't find player");
continue;
}
Err(err) => {
eprintln!("D-Bus error while finding player: {:?}", err);
return;
}
};
println!(
"Connected to \"{}\" ({})",
player.identity(),
player.bus_name()
player.path().as_str(),
player.destination().as_str()
);
connected = true;
magpie.send_json_message(0, &InMsg::Connected);
magpie.send_panel_json_async(0, &InMsg::Connected).await;
let metadata = player.get_metadata().unwrap();
let mut metadata_tracker = MetadataTracker::new(&mut magpie, metadata);
let mut events = match player.events() {
Ok(events) => events,
match player_main(&player, &mut magpie).await {
Ok(()) => {}
Err(err) => {
eprintln!("Player events D-Bus error: {:?}", err);
continue;
}
};
loop {
let event = match events.next() {
None => break,
Some(Ok(e)) => e,
Some(Err(err)) => {
eprintln!("D-Bus error while reading player events: {:?}", err);
continue;
}
};
use mpris::Event::*;
let in_msg = match event {
Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)),
Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)),
Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)),
LoopingChanged(status) => {
use mpris::LoopStatus::*;
let status = match status {
None => LoopStatus::None,
Track => LoopStatus::Track,
Playlist => LoopStatus::Playlist,
};
Some(InMsg::LoopingChanged(status))
}
ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }),
VolumeChanged(volume) => Some(InMsg::VolumeChanged {
volume: volume as f32,
}),
PlayerShutDown => None,
TrackChanged(metadata) => {
metadata_tracker.update(&mut magpie, metadata);
None
}
_ => {
eprintln!("Unhandled MPRIS message: {:?}", event);
None
}
};
if let Some(msg) = in_msg {
magpie.send_json_message(0, &msg);
eprintln!("D-Bus error while connected to player: {:?}", err);
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use zbus::fdo::DBusProxy;
use zbus::zvariant::Value;
use zbus::{dbus_proxy, Connection, Result};
pub type MetadataMap<'a> = HashMap<String, Value<'a>>;
#[dbus_proxy(
interface = "org.mpris.MediaPlayer2.Player",
default_path = "/org/mpris/MediaPlayer2"
)]
trait Player {
fn next(&self) -> Result<()>;
fn previous(&self) -> Result<()>;
fn pause(&self) -> Result<()>;
fn play_pause(&self) -> Result<()>;
fn stop(&self) -> Result<()>;
fn play(&self) -> Result<()>;
fn seek(&self, offset: i64) -> Result<()>;
#[dbus_proxy(property)]
fn playback_status(&self) -> Result<String>;
#[dbus_proxy(property)]
fn position(&self) -> Result<i64>;
#[dbus_proxy(property)]
fn metadata(&self) -> Result<MetadataMap>;
}
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
let dbus = DBusProxy::new(connection).await?;
let names = dbus.list_names().await?;
for name in names {
let name = name.as_str().to_string();
if name.starts_with("org.mpris.MediaPlayer2") {
let player = PlayerProxy::builder(connection)
.destination(name)?
.build()
.await?;
return Ok(Some(player));
}
}
Ok(None)
}

View File

@ -0,0 +1,19 @@
[package]
name = "canary-notifications"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "canary-notifications"
path = "src/main.rs"
required-features = ["bin"]
[dependencies]
async-std = { version = "1.12", optional = true, features = ["attributes"] }
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zbus = { version = "3.5", optional = true }
[features]
bin = ["dep:async-std", "dep:canary-magpie", "dep:zbus"]

View File

@ -0,0 +1,27 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::{Deserialize, Serialize};
pub use serde;
pub use serde_json;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Contents {
/// The optional name of the application sending the notification.
pub app_name: Option<String>,
/// The summary text briefly describing the notification.
pub summary: String,
/// The optional detailed body text.
pub body: Option<String>,
/// The timeout time in milliseconds since the display of the notification
/// at which the notification should automatically close.
pub timeout: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum OutMsg {}

View File

@ -0,0 +1,143 @@
use std::collections::HashMap;
use std::future::pending;
use std::path::PathBuf;
use async_std::channel::{unbounded, Sender};
use async_std::os::unix::net::UnixStream;
use canary_magpie::protocol::*;
use canary_notifications::Contents;
use zbus::{dbus_interface, zvariant::Value, ConnectionBuilder, SignalContext};
pub type MagpieClient = ClientMessenger<UnixStream>;
pub struct Notifications {
module_path: PathBuf,
magpie_sender: Sender<MagpieServerMsg>,
next_id: u32,
}
#[dbus_interface(name = "org.freedesktop.Notifications")]
impl Notifications {
fn get_capabilities(&self) -> Vec<String> {
vec!["body", "body-markup", "actions", "icon-static"]
.into_iter()
.map(ToString::to_string)
.collect()
}
#[dbus_interface(out_args("name", "vendor", "version", "spec_version"))]
fn get_server_information(&self) -> zbus::fdo::Result<(String, String, String, String)> {
Ok((
"canary-notifications".to_string(),
"Canary Development Team".to_string(),
"0.1.0".to_string(),
"1.2".to_string(),
))
}
async fn notify(
&mut self,
app_name: String,
replaces_id: u32,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: HashMap<String, Value<'_>>,
timeout: i32,
) -> u32 {
let timeout = match timeout {
-1 => Some(5000), // default timeout
0 => None,
t => Some(t),
};
let contents = Contents {
app_name: Some(app_name).filter(|s| !s.is_empty()),
summary,
body: Some(body).filter(|s| !s.is_empty()),
timeout,
};
let id = self.next_id;
self.next_id += 1;
let msg = CreatePanel {
id,
protocol: "tebibyte-media.desktop.notification".to_string(),
script: self.module_path.to_owned(),
init_msg: serde_json::to_vec(&contents).unwrap(),
};
if let Some(delay_ms) = contents.timeout.clone() {
let delay = std::time::Duration::from_millis(delay_ms as _);
let magpie_sender = self.magpie_sender.to_owned();
async_std::task::spawn(async move {
async_std::task::sleep(delay).await;
magpie_sender
.send(MagpieServerMsg::ClosePanel(ClosePanel { id }))
.await
.unwrap();
});
}
self.magpie_sender
.send(MagpieServerMsg::CreatePanel(msg))
.await
.unwrap();
id
}
fn close_notification(&self, id: u32) {}
#[dbus_interface(signal)]
async fn notification_closed(ctx: &SignalContext<'_>, id: u32, reason: u32)
-> zbus::Result<()>;
#[dbus_interface(signal)]
async fn action_invoked(
ctx: &SignalContext<'_>,
id: u32,
action_key: String,
) -> zbus::Result<()>;
}
#[async_std::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
let module_path = args
.get(1)
.expect("Please pass a path to a Canary script!")
.to_owned()
.into();
let sock_path = find_socket();
let socket = UnixStream::connect(sock_path).await.unwrap();
let mut magpie = MagpieClient::new(socket);
let (magpie_sender, magpie_receiver) = unbounded();
let notifications = Notifications {
magpie_sender,
next_id: 0,
module_path,
};
let _ = ConnectionBuilder::session()
.unwrap()
.name("org.freedesktop.Notifications")
.unwrap()
.serve_at("/org/freedesktop/Notifications", notifications)
.unwrap()
.build()
.await
.unwrap();
async_std::task::spawn(async move {
while let Ok(msg) = magpie_receiver.recv().await {
magpie.send_async(&msg).await.unwrap();
}
});
pending::<()>().await;
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary::{CursorEventKind, Panel, Runtime, Script};
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
use eframe::egui;
use std::time::Instant;
@ -31,7 +31,9 @@ struct App {
panels: Vec<PanelWindow>,
next_idx: usize,
last_update: Instant,
protocol_buf: String,
bind_message_buf: String,
panel_bg: egui::Color32,
}
impl App {
@ -46,7 +48,9 @@ impl App {
panels: vec![],
next_idx: 0,
last_update: Instant::now(),
protocol_buf: String::new(),
bind_message_buf: String::new(),
panel_bg: egui::Color32::TRANSPARENT,
}
}
}
@ -56,12 +60,18 @@ impl eframe::App for App {
ctx.request_repaint();
egui::SidePanel::left("left_panel").show(ctx, |ui| {
ui.heading("New Panel");
ui.label("Protocol name:");
ui.text_edit_singleline(&mut self.protocol_buf);
ui.label("Bind message:");
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
ui.add(text_edit);
if ui.button("Bind Panel").clicked() {
let msg = self.bind_message_buf.as_bytes().to_vec();
let panel = self.script.create_panel(msg).unwrap();
let panel = self.script.create_panel(&self.protocol_buf, msg).unwrap();
let index = self.next_idx;
self.next_idx += 1;
@ -70,10 +80,19 @@ impl eframe::App for App {
index,
msg_buf: String::new(),
show_msg: false,
current_size: Default::default(),
};
self.panels.push(panel);
}
ui.separator();
ui.heading("Global Settings");
ui.horizontal(|ui| {
ui.label("Panel background color: ");
ui.color_edit_button_srgba(&mut self.panel_bg);
});
});
let dt = self.last_update.elapsed().as_secs_f32();
@ -81,7 +100,7 @@ impl eframe::App for App {
for panel in self.panels.iter_mut() {
panel.panel.update(dt);
panel.show(ctx);
panel.show(self.panel_bg, ctx);
}
}
}
@ -91,80 +110,91 @@ pub struct PanelWindow {
pub index: usize,
pub msg_buf: String,
pub show_msg: bool,
pub current_size: egui::Vec2,
}
impl PanelWindow {
pub fn show(&mut self, ctx: &egui::Context) {
pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) {
let frame = egui::Frame::window(&ctx.style()).fill(bg);
let window_id = egui::Id::new(format!("panel_{}", self.index));
egui::Window::new("Panel").id(window_id).show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.checkbox(&mut self.show_msg, "Show Message Editor");
});
egui::Window::new("Panel")
.frame(frame)
.id(window_id)
.show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.checkbox(&mut self.show_msg, "Show Message Editor");
});
let size = egui::vec2(800.0, 800.0);
let sense = egui::Sense {
click: true,
drag: true,
focusable: true,
};
let (rect, response) = ui.allocate_at_least(size, sense);
if let Some(hover_pos) = response.hover_pos() {
let local = (hover_pos - rect.left_top()) / rect.size();
let norm = local * 2.0 - egui::vec2(1.0, 1.0);
let x = norm.x;
let y = -norm.y;
let pos = canary::Vec2 { x, y };
let kind = if response.drag_started() {
CursorEventKind::Select
} else if response.drag_released() {
CursorEventKind::Deselect
} else if response.dragged() {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
let sense = egui::Sense {
click: true,
drag: true,
focusable: true,
};
self.panel.on_cursor_event(kind, pos);
}
let desired_size = ui.available_size();
let response = ui.allocate_response(desired_size, sense);
let rect = response.rect;
let texture = egui::TextureId::Managed(0);
let uv = egui::pos2(0.0, 0.0);
let mut mesh = egui::Mesh::with_texture(texture);
if rect.size() != self.current_size {
let size = rect.size();
self.current_size = size;
let commands = self.panel.draw();
for command in commands.into_iter() {
let voff = mesh.vertices.len() as u32;
match command {
canary::DrawCommand::Mesh { vertices, indices } => {
for v in vertices.iter() {
use egui::epaint::Vertex;
let pos = egui::pos2(v.position.x, -v.position.y);
let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5);
let pos = rect.left_top() + pos * rect.size();
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
let v = Vertex { pos, uv, color };
mesh.vertices.push(v);
}
for i in indices.iter() {
mesh.indices.push(i + voff);
}
}
_ => unimplemented!(),
let size = canary::Vec2::new(size.x, size.y);
self.panel.on_resize(size * PX_PER_MM);
}
}
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
if let Some(hover_pos) = response.hover_pos() {
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
let pos = canary::Vec2::new(local.x, local.y);
response
});
let kind = if response.drag_started() {
CursorEventKind::Select
} else if response.drag_released() {
CursorEventKind::Deselect
} else if response.dragged() {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
};
self.panel.on_cursor_event(kind, pos);
}
let texture = egui::TextureId::Managed(0);
let uv = egui::pos2(0.0, 0.0);
let mut mesh = egui::Mesh::with_texture(texture);
let commands = self.panel.draw();
for command in commands.into_iter() {
let voff = mesh.vertices.len() as u32;
match command {
canary::DrawCommand::Mesh { vertices, indices } => {
for v in vertices.iter() {
use egui::epaint::Vertex;
let pos = v.position / PX_PER_MM;
let pos = egui::pos2(pos.x, pos.y);
let pos = pos + rect.left_top().to_vec2();
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
let v = Vertex { pos, uv, color };
mesh.vertices.push(v);
}
for i in indices.iter() {
mesh.indices.push(i + voff);
}
}
_ => unimplemented!(),
}
}
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
response
});
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
egui::Window::new("Message Editor")

1
book/.gitignore vendored
View File

@ -1 +0,0 @@
book

View File

@ -1,6 +0,0 @@
[book]
authors = ["marceline-cramer"]
language = "en"
multilingual = false
src = "src"
title = "Canary GUI Book"

View File

@ -1,30 +0,0 @@
# Summary
[Introduction](introduction.md)
[History](history.md)
- [Implementation](impl/README.md)
- [Development Process](impl-rs/process.md)
- [Usage](impl-rs/usage.md)
- [Backends](impl-rs/backends.md)
- [Examples](impl-rs/examples.md)
- [Ecosystem](ecosystem/README.md)
- [Messages](ecosystem/messages.md)
- [Protocols](ecosystem/protocols.md)
- [Finding Scripts](ecosystem/finding-scripts.md)
- [Fonts](ecosystem/fonts.md)
- [Localization](ecosystem/localization.md)
- [Rendering](rendering/README.md)
- [Graphics State](rendering/state.md)
- [Primitives](rendering/primitives.md)
- [Canvases](rendering/canvases.md)
- [Tessellation](rendering/tessellation.md)
- [Text](rendering/text.md)
- [Input](input/README.md)
- [Pointer](input/pointer.md)
- [Text](input/text.md)
---
[Glossary](glossary.md)
[Credits](credits.md)

View File

@ -1 +0,0 @@
# Credits

View File

@ -1 +0,0 @@
# Ecosystem

View File

@ -1 +0,0 @@
# Finding Scripts

View File

@ -1 +0,0 @@
# Fonts

View File

@ -1 +0,0 @@
# Localization

View File

@ -1 +0,0 @@
# Messages

View File

@ -1 +0,0 @@
# Protocols

View File

@ -1 +0,0 @@
# Glossary

View File

@ -1,16 +0,0 @@
# History
Canary was originally conceived in early 2021 as a WebAssembly-based,
minimalistic UI framework during the development of [Mondradiko](https://mondradiko.github.io),
where it was a dedicated subsystem of a larger game engine. When the new UI
system turned out to be even more powerful than originally expected, it was
decided that the UI code would be broken out into a separate project. The
Mondradiko community voted to name it "Canary" (the other contenders were
"Robin" and "Magpie"), and it was given [a new repository](https://github.com/mondradiko/canary).
However, before Canary could be fully fleshed-out, development on Mondradiko
was ceased and there was no reason to continue working on Canary.
In mid-2022, development was started back up, as a member project of
[Tebibyte Media](https://tebibyte.media). This new community of free software
enthusiasts had new interest in Canary apart from its usage in a larger game
engine, so development was restarted.

View File

@ -1 +0,0 @@
# Backends

View File

@ -1 +0,0 @@
# Examples

View File

@ -1,23 +0,0 @@
# Development Process
# Adding New Features
To keep Canary as minimal as possible we adopt a conservative policy for what
features are added to its specification. This is to avoid the feature-creep that
plagues large UI and UX frameworks in the long run. The following reasons are
general guidelines for what features should and should not be added to Canary.
## Reasons to add a feature
The feature provides a clear benefit to a cultural class of users. For example,
Arabic speakers will require that text can be rendered right-to-left.
The feature reduces the resource usage of scripts.
## Reasons NOT to add a feature
The feature adds more complexity to the host than is removed from scripts.
The feature only applies to certain host configurations.
The feature can be effectively emulated in a script.

View File

@ -1,4 +0,0 @@
# Implementation
This chapter discusses the design and usage of [canary-rs](https://git.tebibyte.media/canary/canary-rs),
the canonical implementation of Canary.

View File

@ -1 +0,0 @@
# Input

View File

@ -1 +0,0 @@
# Pointer

View File

@ -1 +0,0 @@
# Text

View File

@ -1 +0,0 @@
# Introduction

View File

@ -1 +0,0 @@
# Rendering

View File

@ -1 +0,0 @@
# Canvases

View File

@ -1 +0,0 @@
# Primitives

View File

@ -1 +0,0 @@
# Graphics State

View File

@ -1 +0,0 @@
# Tessellation

View File

@ -1 +0,0 @@
# Text

View File

@ -1,4 +1,38 @@
#!/bin/sh
# Depends on: `rg` (ripgrep)
! rg --multiline --files-without-match --glob '*.rs' --pcre2 '(?<!\n)((//)|(#)) Copyright \(c\) \d+ [A-z, ]+\n((//)|(#)) SPDX-License-Identifier: .*\n'
set -e
# check if we have tomcat(1)
if ! command -v tomcat >/dev/null 2>&1; then
printf "%s: Missing dependency: tomcat(1)\n"
exit 69 # sysexits(3) EX_UNAVAILABLE
fi
dir="$(pwd | sed 's/\//\n/g' | tail -n 1)"
for toml in $(find "$PWD" -name "Cargo.toml"); do
printf "Project: %s\n" "$(tomcat package.name "$toml")"
toml_lic="$(tomcat package.license "$toml")"
if ! test -n "$toml_lic"; then
printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\
sed "s/^.\+$dir\///g")"
continue 2
fi
for file in $(find "$(printf "%s\n" "$toml" |\
sed 's/Cargo\.toml/src/g')" -name "*.rs")
do
info="$(head -n 2 "$file")"
if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
then
printf "%s: Missing or malformed license information\n" \
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
fi
if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\
sed -n '/\/\/ Copyright (c) .\+/p')"
then
printf "%s: Missing or malformed copyright holder information\n" \
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
fi
done
done

View File

@ -7,11 +7,17 @@ use super::*;
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
pub fn bind_panel<T: BindPanel>(panel: u32, msg: u32) -> u32 {
pub fn bind_panel(
cb: impl Fn(Panel, Message, Message) -> Box<dyn PanelImpl>,
panel: u32,
protocol: u32,
msg: u32,
) -> u32 {
unsafe {
let panel = Panel(panel);
let protocol = Message(protocol);
let msg = Message(msg);
let panel_impl = T::bind(panel, msg);
let panel_impl = cb(panel, protocol, msg);
let id = PANEL_IMPLS.len() as u32;
PANEL_IMPLS.push(panel_impl);
id

View File

@ -9,10 +9,10 @@ pub mod abi;
#[macro_export]
macro_rules! export_abi {
($panel_impl: ident) => {
($bind_panel: ident) => {
#[no_mangle]
pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 {
::canary_script::api::abi::bind_panel::<$panel_impl>(panel, msg)
pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 {
::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg)
}
#[no_mangle]
@ -42,10 +42,6 @@ macro_rules! export_abi {
};
}
pub trait BindPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl>;
}
pub trait PanelImpl {
fn update(&mut self, dt: f32);
fn draw(&mut self);
@ -98,19 +94,29 @@ impl Panel {
self.draw_indexed(&vertices, &indices);
}
pub fn send_message(&self, message: &[u8]) {
unsafe { panel_send_message(self.0, message.as_ptr() as u32, message.len() as u32) }
}
}
#[repr(transparent)]
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub struct Font(u32);
impl Font {
pub fn new(family: &str) -> Self {
unsafe { Self(font_load(family.as_ptr() as u32, family.len() as u32)) }
}
/// Retrieves the script-local identifier of this font.
pub fn get_id(&self) -> u32 {
self.0
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct TextLayout(u32);
impl TextLayout {
@ -255,10 +261,10 @@ impl DrawContext {
let delta = PI / 4.0 / spoke_num;
let (mut theta, limit) = match corner {
Corner::TopRight => (0.0, FRAC_PI_2),
Corner::BottomRight => (FRAC_PI_2 * 3.0, PI * 2.0),
Corner::BottomLeft => (PI, FRAC_PI_2 * 3.0),
Corner::TopLeft => (FRAC_PI_2, PI),
Corner::TopRight => (FRAC_PI_2 * 3.0, PI * 2.0),
Corner::BottomRight => (0.0, FRAC_PI_2),
Corner::BottomLeft => (FRAC_PI_2, PI),
Corner::TopLeft => (PI, FRAC_PI_2 * 3.0),
};
let mut last_spoke = Vec2::from_angle(theta) * radius + center;
@ -305,10 +311,10 @@ impl DrawContext {
rect
};
let v1 = rect.bl;
let v2 = Vec2::new(rect.bl.x, rect.tr.y);
let v3 = Vec2::new(rect.tr.x, rect.bl.y);
let v4 = rect.tr;
let v1 = rect.tl;
let v2 = Vec2::new(rect.tl.x, rect.br.y);
let v3 = Vec2::new(rect.br.x, rect.tl.y);
let v4 = rect.br;
self.draw_triangle_noclip(v1, v2, v3, color);
self.draw_triangle_noclip(v2, v3, v4, color);
@ -333,48 +339,48 @@ impl DrawContext {
let mut inner_rect = rect;
let inset = rect.inset(radius);
if corners.intersects(CornerFlags::BOTTOM) {
inner_rect.bl.y += radius;
let mut bottom_edge = Rect {
bl: rect.bl,
tr: Vec2::new(rect.tr.x, rect.bl.y + radius),
};
if corners.contains(CornerFlags::BOTTOM_LEFT) {
bottom_edge.bl.x += radius;
self.draw_quarter_circle(Corner::BottomLeft, inset.bl, radius, color);
}
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
bottom_edge.tr.x -= radius;
self.draw_quarter_circle(Corner::BottomRight, inset.br(), radius, color);
}
self.draw_rect(bottom_edge, color);
}
if corners.intersects(CornerFlags::TOP) {
inner_rect.tr.y -= radius;
inner_rect.tl.y += radius;
let mut top_edge = Rect {
bl: Vec2::new(rect.bl.x, rect.tr.y - radius),
tr: rect.tr,
tl: rect.tl,
br: Vec2::new(rect.br.x, rect.tl.y + radius),
};
if corners.contains(CornerFlags::TOP_LEFT) {
top_edge.bl.x += radius;
self.draw_quarter_circle(Corner::TopLeft, inset.tl(), radius, color);
top_edge.tl.x += radius;
self.draw_quarter_circle(Corner::TopLeft, inset.tl, radius, color);
}
if corners.contains(CornerFlags::TOP_RIGHT) {
top_edge.tr.x -= radius;
self.draw_quarter_circle(Corner::TopRight, inset.tr, radius, color);
top_edge.br.x -= radius;
self.draw_quarter_circle(Corner::TopRight, inset.tr(), radius, color);
}
self.draw_rect(top_edge, color);
}
if corners.intersects(CornerFlags::BOTTOM) {
inner_rect.br.y -= radius;
let mut bottom_edge = Rect {
tl: Vec2::new(rect.tl.x, rect.br.y - radius),
br: rect.br,
};
if corners.contains(CornerFlags::BOTTOM_LEFT) {
bottom_edge.tl.x += radius;
self.draw_quarter_circle(Corner::BottomLeft, inset.bl(), radius, color);
}
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
bottom_edge.br.x -= radius;
self.draw_quarter_circle(Corner::BottomRight, inset.br, radius, color);
}
self.draw_rect(bottom_edge, color);
}
self.draw_rect(inner_rect, color);
}
@ -447,4 +453,6 @@ extern "C" {
fn message_get_len(id: u32) -> u32;
fn message_get_data(id: u32, ptr: u32);
fn panel_send_message(id: u32, message_ptr: u32, message_len: u32);
}

View File

@ -11,78 +11,78 @@ pub mod api;
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
pub struct Rect {
pub bl: Vec2,
pub tr: Vec2,
pub tl: Vec2,
pub br: Vec2,
}
impl Rect {
pub const NEG_INFINITY: Self = Self {
bl: Vec2::splat(f32::INFINITY),
tr: Vec2::splat(f32::NEG_INFINITY),
tl: Vec2::splat(f32::INFINITY),
br: Vec2::splat(f32::NEG_INFINITY),
};
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
Self {
bl: xy,
tr: xy + size,
tl: xy,
br: xy + size,
}
}
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
Self {
bl: center - radius,
tr: center + radius,
tl: center - radius,
br: center + radius,
}
}
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
Self {
bl: tri.v1.min(tri.v2).min(tri.v3),
tr: tri.v1.max(tri.v2).max(tri.v3),
tl: tri.v1.min(tri.v2).min(tri.v3),
br: tri.v1.max(tri.v2).max(tri.v3),
}
}
pub fn inset(&self, d: f32) -> Self {
Self {
bl: self.bl + d,
tr: self.tr - d,
tl: self.tl + d,
br: self.br - d,
}
}
pub fn tl(&self) -> Vec2 {
Vec2::new(self.bl.x, self.tr.y)
pub fn bl(&self) -> Vec2 {
Vec2::new(self.tl.x, self.br.y)
}
pub fn br(&self) -> Vec2 {
Vec2::new(self.tr.x, self.bl.y)
pub fn tr(&self) -> Vec2 {
Vec2::new(self.br.x, self.tl.y)
}
pub fn offset(&self, offset: Vec2) -> Self {
Self {
bl: self.bl + offset,
tr: self.tr + offset,
tl: self.tl + offset,
br: self.br + offset,
}
}
pub fn scale(&self, scale: f32) -> Self {
Self {
bl: self.bl * scale,
tr: self.tr * scale,
tl: self.tl * scale,
br: self.br * scale,
}
}
pub fn is_valid(&self) -> bool {
self.bl.cmplt(self.tr).all()
self.tl.cmplt(self.br).all()
}
pub fn intersects_rect(&self, other: &Self) -> bool {
self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all()
self.tl.cmple(other.br).all() && self.br.cmpge(other.tl).all()
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let clipped = Self {
bl: self.bl.max(other.bl),
tr: self.tr.min(other.tr),
tl: self.tl.max(other.tl),
br: self.br.min(other.br),
};
if clipped.is_valid() {
@ -92,27 +92,41 @@ impl Rect {
}
}
pub fn union(&self, other: &Self) -> Self {
Self {
tl: self.tl.min(other.tl),
br: self.br.max(other.br),
}
}
pub fn union_point(&self, point: Vec2) -> Self {
Self {
tl: self.tl.min(point),
br: self.br.max(point),
}
}
pub fn contains_rect(&self, other: &Self) -> bool {
self.bl.x < other.bl.x
&& self.bl.y < other.bl.y
&& self.tr.x > other.tr.x
&& self.tr.y > other.tr.y
self.tl.x < other.tl.x
&& self.tl.y < other.tl.y
&& self.br.x > other.br.x
&& self.br.y > other.br.y
}
pub fn contains_point(&self, xy: Vec2) -> bool {
self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y
self.tl.x < xy.x && self.tl.y < xy.y && self.br.x > xy.x && self.br.y > xy.y
}
pub fn size(&self) -> Vec2 {
self.tr - self.bl
self.br - self.tl
}
pub fn width(&self) -> f32 {
self.tr.x - self.bl.x
self.br.x - self.tl.x
}
pub fn height(&self) -> f32 {
self.tr.y - self.bl.y
self.br.y - self.tl.y
}
}
@ -159,15 +173,21 @@ impl Color {
)
}
pub fn alpha_multiply(&self, mul: u8) -> Self {
pub const fn alpha_multiply(&self, mul: u8) -> Self {
let a = self.0 as u8 as u16;
let multiplied = ((a * (mul as u16)) >> 8) as u8;
self.with_alpha(multiplied)
}
pub fn with_alpha(&self, alpha: u8) -> Self {
pub const fn with_alpha(&self, alpha: u8) -> Self {
Self(self.0 & 0xffffff00 | alpha as u32)
}
pub fn lerp(self, target: Self, blend: f32) -> Self {
let s: glam::Vec4 = self.into();
let o: glam::Vec4 = target.into();
(o * blend + s * (1.0 - blend)).into()
}
}
#[repr(C)]

View File

@ -0,0 +1,9 @@
[package]
name = "canary-textwrap"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
[dependencies]
canary-script = { path = "../script" }
textwrap = "0.16"

248
crates/textwrap/src/lib.rs Normal file
View File

@ -0,0 +1,248 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use canary_script::api::{DrawContext, Font, TextLayout};
use canary_script::{Color, Vec2};
#[derive(Default)]
pub struct TextCache {
layouts: Vec<OwnedText>,
fonts: Vec<HashMap<String, usize>>,
}
impl TextCache {
pub fn insert(&mut self, font: Font, text: &str) -> (usize, f32) {
let font_idx = font.get_id() as usize;
if let Some(font_cache) = self.fonts.get(font_idx) {
if let Some(layout_idx) = font_cache.get(text) {
return (*layout_idx, self.get(*layout_idx).width);
}
} else {
let new_size = font_idx + 1;
self.fonts.resize_with(new_size, Default::default);
}
let index = self.layouts.len();
let layout = TextLayout::new(&font, text);
let width = layout.get_bounds().width().max(0.0);
self.layouts.push(OwnedText {
font,
layout,
width,
});
self.fonts[font_idx].insert(text.to_string(), index);
(index, width)
}
pub fn get(&self, index: usize) -> Text<'_> {
self.layouts[index].borrow()
}
}
/// Processed text that can be laid out.
pub struct Content {
pub words: Vec<Word>,
}
impl Content {
pub fn from_plain(cache: &mut TextCache, font: Font, text: &str) -> Self {
use textwrap::word_splitters::{split_words, WordSplitter};
use textwrap::WordSeparator;
let separator = WordSeparator::new();
let words = separator.find_words(text);
// TODO: crate feature to enable hyphenation support?
let splitter = WordSplitter::NoHyphenation;
let split_words = split_words(words, &splitter);
let mut words = Vec::new();
for split_word in split_words {
let (word, word_width) = cache.insert(font, split_word.word);
let (whitespace, whitespace_width) = cache.insert(font, "_");
let (penalty, penalty_width) = cache.insert(font, split_word.penalty);
words.push(Word {
word,
word_width,
whitespace,
whitespace_width,
penalty,
penalty_width,
});
}
Self { words }
}
pub fn layout(&self, cache: &TextCache, width: f32) -> Layout {
use textwrap::wrap_algorithms::wrap_optimal_fit;
let fragments = self.words.as_slice();
let line_widths = &[width as f64];
let penalties = Default::default();
// Should never fail with reasonable input. Check [wrap_optimal_fit] docs for more info.
let wrapped_lines = wrap_optimal_fit(fragments, line_widths, &penalties).unwrap();
let mut lines = Vec::new();
for line in wrapped_lines {
lines.push(Line::from_word_line(cache, line));
}
Layout { lines }
}
}
/// An atomic fragment of processed text that is ready to be laid out.
///
/// May or may not correspond to a single English "word".
///
/// Please see [textwrap::core::Word] and [textwrap::core::Fragment] for more information.
#[derive(Debug)]
pub struct Word {
pub word: usize,
pub word_width: f32,
pub whitespace: usize,
pub whitespace_width: f32,
pub penalty: usize,
pub penalty_width: f32,
}
impl textwrap::core::Fragment for Word {
fn width(&self) -> f64 {
self.word_width as f64
}
fn whitespace_width(&self) -> f64 {
self.whitespace_width as f64
}
fn penalty_width(&self) -> f64 {
self.penalty_width as f64
}
}
#[derive(Debug)]
pub struct OwnedText {
pub font: Font,
pub layout: TextLayout,
pub width: f32,
}
impl OwnedText {
pub fn borrow(&self) -> Text<'_> {
Text {
font: self.font,
layout: &self.layout,
width: self.width,
}
}
}
/// A single piece of renderable text.
#[derive(Debug)]
pub struct Text<'a> {
/// The font that this fragment has been laid out with.
pub font: Font,
/// The draw-ready [TextLayout] of this fragment.
pub layout: &'a TextLayout,
/// The width of this text.
pub width: f32,
}
/// A finished, wrapped text layout.
pub struct Layout {
pub lines: Vec<Line>,
}
impl Layout {
pub fn draw(
&self,
cache: &TextCache,
ctx: &DrawContext,
scale: f32,
line_height: f32,
color: Color,
) {
let mut cursor = Vec2::ZERO;
for line in self.lines.iter() {
let ctx = ctx.with_offset(cursor);
line.draw(cache, &ctx, scale, color);
cursor.y += line_height;
}
}
}
/// A finished line of a layout.
pub struct Line {
pub fragments: Vec<Fragment>,
}
impl Line {
pub fn from_word_line(cache: &TextCache, words: &[Word]) -> Self {
let last_idx = words.len() - 1;
let mut fragments = Vec::new();
let mut add_word = |index: usize, hidden: bool| {
let text = cache.get(index);
fragments.push(Fragment {
font: text.font,
text: index,
offset: Vec2::ZERO,
advance: text.width,
hidden,
});
};
for (idx, word) in words.iter().enumerate() {
add_word(word.word, false);
if idx == last_idx {
add_word(word.penalty, false);
} else {
add_word(word.whitespace, true);
}
}
Self { fragments }
}
pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, color: Color) {
let mut cursor = Vec2::ZERO;
for fragment in self.fragments.iter() {
if !fragment.hidden {
let text = cache.get(fragment.text);
let offset = cursor + fragment.offset;
ctx.draw_text_layout(text.layout, offset, scale, color);
}
cursor.x += fragment.advance * scale;
}
}
}
/// A finished fragment in a layout.
pub struct Fragment {
/// The font of this fragment.
pub font: Font,
/// The index into the [TextCache] of the content of this fragment.
pub text: usize,
/// The offset for drawing the text layout.
pub offset: Vec2,
/// The horizontal advance to draw the next fragment.
pub advance: f32,
/// Whether this fragment should be skipped while drawing.
pub hidden: bool,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -2,6 +2,7 @@
name = "canary-music-player-script"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[lib]
crate-type = ["cdylib"]

View File

@ -1,10 +1,17 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use canary_script::*;
use api::*;
use canary_script::*;
canary_script::export_abi!(MusicPlayerPanel);
canary_script::export_abi!(bind_panel_impl);
pub fn bind_panel_impl(panel: Panel, _protocol: Message, message: Message) -> Box<dyn PanelImpl> {
MusicPlayerPanel::bind(panel, message)
}
const DISPLAY_FONT: &str = "Liberation Sans";
@ -14,19 +21,6 @@ pub struct MusicPlayerPanel {
label: Label,
}
impl BindPanel for MusicPlayerPanel {
fn bind(panel: Panel, message: Message) -> Box<dyn PanelImpl> {
let display_font = Font::new(DISPLAY_FONT);
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
let panel = Self {
panel,
display_font,
label,
};
Box::new(panel)
}
}
impl PanelImpl for MusicPlayerPanel {
fn update(&mut self, dt: f32) {}
@ -39,20 +33,31 @@ impl PanelImpl for MusicPlayerPanel {
self.label.draw(&ctx, offset, size, color);
}
fn on_resize(&mut self, new_size: Vec2) {
}
fn on_resize(&mut self, new_size: Vec2) {}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
fn on_message(&mut self, msg: Message) {
use canary_music_player::{InMsg, serde_json};
use canary_music_player::{serde_json, InMsg};
let msg = msg.to_vec();
let msg = serde_json::from_slice::<InMsg>(&msg);
self.label.set_text(format!("{:#?}", msg));
}
}
impl MusicPlayerPanel {
pub fn bind(panel: Panel, _message: Message) -> Box<dyn PanelImpl> {
let display_font = Font::new(DISPLAY_FONT);
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
let panel = Self {
panel,
display_font,
label,
};
Box::new(panel)
}
}
pub struct Label {
font: Font,
text: String,

View File

@ -10,7 +10,10 @@ crate-type = ["cdylib"]
[dependencies]
glam = "^0.21"
keyframe = "1"
canary-music-player = { path = "../../apps/music-player" }
canary-notifications = { path = "../../apps/notifications" }
canary-script = { path = "../../crates/script" }
canary-textwrap = { path = "../../crates/textwrap" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
wee_alloc = "^0.4"

View File

@ -1,3 +1,6 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::Color;
use keyframe::EasingFunction;

View File

@ -1 +0,0 @@

View File

@ -1,18 +1,36 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub mod anim;
pub mod draw;
pub mod main_menu;
pub mod panel;
pub mod music_player;
pub mod notifications;
pub mod style;
pub mod widgets;
use canary_script::*;
use api::*;
use widgets::Widget;
use canary_script::*;
use main_menu::MainMenuPanel;
use music_player::MusicPlayerPanel;
use notifications::NotificationPanel;
use widgets::Widget;
export_abi!(MainMenuPanel);
export_abi!(bind_panel_impl);
fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
let protocol = protocol.to_vec();
let protocol = String::from_utf8(protocol).unwrap();
match protocol.as_str() {
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
"tebibyte-media.desktop.notification" => NotificationPanel::bind(panel, msg),
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
_ => MainMenuPanel::bind(panel, msg),
}
}
pub const ICON_FONT: &str = "Iosevka Nerd Font";
pub const DISPLAY_FONT: &str = "Homenaje";
@ -23,18 +41,6 @@ pub struct ConfirmationDialogPanel {
dialog: widgets::dialog::Dialog,
}
impl BindPanel for ConfirmationDialogPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let msg = msg.to_vec();
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
use widgets::dialog::*;
let style = DialogStyle::default();
let dialog = Dialog::new(style, &info);
Box::new(Self { panel, dialog })
}
}
impl PanelImpl for ConfirmationDialogPanel {
fn update(&mut self, dt: f32) {
self.dialog.update(dt);
@ -45,9 +51,31 @@ impl PanelImpl for ConfirmationDialogPanel {
self.dialog.draw(&ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
self.dialog.on_cursor_event(kind, at.into());
}
fn on_resize(&mut self, size: Vec2) {
self.dialog.resize(size);
}
fn on_message(&mut self, _msg: Message) {}
}
impl ConfirmationDialogPanel {
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
// let msg = msg.to_vec();
// let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
let info = DialogInfo {
title: "Hello world!".to_string(),
content: "Testing, testing...".to_string(),
responses: vec![DialogResponse::Yes, DialogResponse::No],
};
use widgets::dialog::*;
let style = DialogStyle::default();
let dialog = Dialog::new(style, &info);
Box::new(Self { panel, dialog })
}
}

View File

@ -1,9 +1,13 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::widgets::prelude::*;
use crate::{DrawContext, Rect};
use button::{RectButton, RoundButton, RoundButtonStyle};
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
use palette::Palette;
use shell::{Offset, OffsetAlignment, Popup, Reveal};
use text::LabelText;
@ -12,15 +16,6 @@ pub struct MainMenuPanel {
menu: MainMenu,
}
impl BindPanel for MainMenuPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
Box::new(Self {
panel,
menu: MainMenu::default(),
})
}
}
impl PanelImpl for MainMenuPanel {
fn update(&mut self, dt: f32) {
Widget::update(&mut self.menu, dt);
@ -31,23 +26,35 @@ impl PanelImpl for MainMenuPanel {
Widget::draw(&mut self.menu, &ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
Widget::on_cursor_event(&mut self.menu, kind, at.into());
}
fn on_resize(&mut self, _size: Vec2) {}
fn on_message(&mut self, msg: Message) {}
}
impl MainMenuPanel {
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
Box::new(Self {
panel,
menu: MainMenu::default(),
})
}
}
pub struct MainMenu {
pub menu: Offset<SlotMenu<RoundButton>>,
pub player_info: Reveal<Offset<PlayerInfo>>,
pub inventory: Reveal<Offset<TabMenu>>,
pub palette: Reveal<Offset<Palette>>,
pub settings: Reveal<Offset<SettingsMenu>>,
}
impl MainMenu {
pub const POSITION_X: f32 = -0.40;
pub const SUBMENU_SPACING: f32 = 0.1;
pub const ANCHOR: Vec2 = Vec2::new(100.0, 100.0);
pub const SUBMENU_SPACING: f32 = 15.0;
}
impl Default for MainMenu {
@ -56,12 +63,12 @@ impl Default for MainMenu {
let icons = ["", "", "", "", "", ""];
let button_style = RoundButtonStyle {
radius: 0.05,
spacing: 0.01,
thickness: 0.002,
body_color: Color::WHITE,
ring_color: Color::WHITE,
icon_color: Color::BLACK,
radius: 7.5,
spacing: 1.5,
thickness: 0.4,
body_color: THEME.palette.surface,
ring_color: THEME.palette.surface,
icon_color: THEME.palette.text,
};
let mut buttons = Vec::new();
@ -75,12 +82,13 @@ impl Default for MainMenu {
buttons.push(button);
}
let menu = SlotMenu::new(buttons, 0.18);
let menu = Offset::new(menu, Vec2::new(Self::POSITION_X, 0.0));
let menu = SlotMenu::new(buttons, 30.0);
let menu = Offset::new(menu, Self::ANCHOR);
let submenu_spacing_left = Vec2::new(Self::POSITION_X - Self::SUBMENU_SPACING, 0.0);
let submenu_spacing_right = Vec2::new(Self::POSITION_X + Self::SUBMENU_SPACING, 0.0);
let reveal_slide = -0.02;
let submenu_spacing = Vec2::new(Self::SUBMENU_SPACING, 0.0);
let submenu_spacing_left = Self::ANCHOR - submenu_spacing;
let submenu_spacing_right = Self::ANCHOR + submenu_spacing;
let reveal_slide = -5.0;
let reveal_duration = 0.1;
let player_info = PlayerInfo::new();
@ -96,6 +104,15 @@ impl Default for MainMenu {
let inventory = Offset::new(inventory, submenu_spacing_right);
let inventory = Reveal::new(inventory, reveal_slide, reveal_duration);
let palette = Palette::new(Default::default());
let palette = Offset::new_aligned(
palette,
submenu_spacing_left,
OffsetAlignment::End,
OffsetAlignment::Center,
);
let palette = Reveal::new(palette, -reveal_slide, reveal_duration);
let settings = SettingsMenu::new();
let settings = Offset::new(settings, submenu_spacing_right);
let settings = Reveal::new(settings, reveal_slide, reveal_duration);
@ -104,6 +121,7 @@ impl Default for MainMenu {
menu,
player_info,
inventory,
palette,
settings,
}
}
@ -114,6 +132,7 @@ impl Container for MainMenu {
f(&mut self.menu);
f(&mut self.player_info);
f(&mut self.inventory);
f(&mut self.palette);
f(&mut self.settings);
}
@ -133,8 +152,14 @@ impl Container for MainMenu {
self.player_info.hide();
self.inventory.hide();
}
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(),
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(),
SlotMenuEvent::SubmenuOpen(4) => {
self.palette.show();
self.settings.show();
}
SlotMenuEvent::SubmenuClose(4) => {
self.palette.hide();
self.settings.hide();
}
_ => {}
};
}
@ -144,14 +169,16 @@ pub struct PlayerInfo {
width: f32,
height: f32,
rounding: f32,
color: Color,
}
impl PlayerInfo {
pub fn new() -> Self {
Self {
width: 0.5,
height: 0.9,
rounding: 0.02,
width: 70.0,
height: 120.0,
rounding: 5.0,
color: THEME.palette.surface,
}
}
}
@ -164,7 +191,7 @@ impl RectBounds for PlayerInfo {
impl Widget for PlayerInfo {
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE);
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color);
}
}
@ -175,7 +202,7 @@ pub struct Inventory {
impl Inventory {
pub fn new(available_width: f32) -> (Self, f32) {
let height = 1.28;
let height = 1024.0;
(
Self {
@ -189,8 +216,8 @@ impl Inventory {
impl Widget for Inventory {
fn draw(&mut self, ctx: &DrawContext) {
let box_size = 0.06;
let box_margin = 0.02;
let box_size = 12.0;
let box_margin = 4.0;
let box_stride = box_size + box_margin;
let grid_width = (self.width / box_stride).floor() as usize;
@ -227,7 +254,7 @@ impl SettingsMenu {
("Log Out", ""),
];
let button_size = Vec2::new(0.4, 0.1);
let button_size = Vec2::new(90.0, 20.0);
let button_rect = Rect::from_xy_size(Vec2::new(0.0, -button_size.y / 2.0), button_size);
let mut buttons = Vec::new();
@ -248,7 +275,7 @@ impl SettingsMenu {
buttons.push(button);
}
let menu = SlotMenu::new(buttons, 0.12);
let menu = SlotMenu::new(buttons, 25.0);
Self {
menu,

View File

@ -0,0 +1,430 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use api::*;
use canary_script::*;
use canary_music_player::{AlbumInfo, OutMsg, PlaybackStatus, ProgressChanged, TrackInfo};
use crate::widgets::prelude::*;
use button::{RoundButton, RoundButtonStyle};
use dialog::{DialogBodyStyle, DialogFooterStyle};
use shell::Offset;
use slider::Slider;
use text::{HorizontalAlignment, Label, LabelText};
pub struct MusicPlayerPanel {
panel: Panel,
widget: Option<MusicPlayerWidget>,
disconnected: Offset<Label>,
}
impl PanelImpl for MusicPlayerPanel {
fn update(&mut self, dt: f32) {
if let Some(widget) = self.widget.as_mut() {
Widget::update(widget, dt);
}
}
fn draw(&mut self) {
let ctx = DrawContext::new(self.panel);
if let Some(widget) = self.widget.as_mut() {
Widget::draw(widget, &ctx);
} else {
self.disconnected.draw(&ctx);
}
}
fn on_resize(&mut self, new_size: Vec2) {
self.disconnected.set_offset(new_size / 2.0);
if let Some(widget) = self.widget.as_mut() {
widget.resize(new_size);
}
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if let Some(widget) = self.widget.as_mut() {
Widget::on_cursor_event(widget, kind, at);
}
}
fn on_message(&mut self, msg: Message) {
use canary_music_player::{serde_json::from_slice, InMsg};
let msg = msg.to_vec();
let msg: InMsg = match from_slice(&msg) {
Ok(msg) => msg,
Err(_) => return,
};
use InMsg::*;
match (self.widget.as_mut(), msg) {
(Some(_), Disconnected) => self.widget = None,
(None, Connected) => self.widget = Some(MusicPlayerWidget::new(self.panel)),
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
(Some(widget), TrackChanged(info)) => widget.update_track(info),
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
_ => {}
}
}
}
impl MusicPlayerPanel {
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
let dc_text = LabelText {
font: Font::new(crate::DISPLAY_FONT),
text: "Disconnected".to_string(),
};
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
let disconnected = Offset::new(disconnected, Vec2::ZERO);
Box::new(Self {
panel,
widget: None,
disconnected,
})
}
}
pub struct MusicPlayerStyle {
pub body: DialogBodyStyle,
pub footer: DialogFooterStyle,
pub rounding: f32,
pub art_margin: f32,
pub button_spacing: f32,
pub slider_height: f32,
}
impl Default for MusicPlayerStyle {
fn default() -> Self {
Self {
body: Default::default(),
footer: Default::default(),
rounding: 5.0,
art_margin: 5.0,
button_spacing: 15.0,
slider_height: 7.5,
}
}
}
pub struct MusicPlayerWidget {
panel: Panel,
artist: Offset<Label>,
album: Offset<Label>,
track: Offset<Label>,
previous: Offset<RoundButton>,
play: Offset<RoundButton>,
next: Offset<RoundButton>,
position: Offset<Label>,
duration: Offset<Label>,
slider: Slider,
style: MusicPlayerStyle,
art_rect: Rect,
body_rect: Rect,
footer_rect: Rect,
position_secs: f32,
duration_secs: f32,
position_dirty: bool,
position_updating: bool,
status: PlaybackStatus,
}
impl Container for MusicPlayerWidget {
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
f(&mut self.artist);
f(&mut self.album);
f(&mut self.track);
f(&mut self.previous);
f(&mut self.play);
f(&mut self.next);
f(&mut self.position);
f(&mut self.duration);
f(&mut self.slider);
}
fn update(&mut self, dt: f32) {
let position_display = if let Some(position) = self.slider.has_update() {
self.position_updating = true;
Some(position * self.duration_secs)
} else if self.position_updating {
let position = self.slider.get_position() * self.duration_secs;
let offset = position - self.position_secs;
let msg = OutMsg::Seek { offset };
self.send_message(&msg);
self.position_secs = position;
self.position_updating = false;
Some(position)
} else if let PlaybackStatus::Playing = self.status {
self.position_secs += dt;
Some(self.position_secs)
} else if self.position_dirty {
self.position_dirty = false;
Some(self.position_secs)
} else {
None
};
if let Some(position) = position_display {
self.position_dirty = false;
self.position.set_text(&Self::format_time(position));
self.slider
.set_position(self.position_secs / self.duration_secs);
}
if self.previous.was_clicked() {
self.send_message(&OutMsg::Previous);
}
if self.play.was_clicked() {
self.send_message(&OutMsg::PlayPause);
}
if self.next.was_clicked() {
self.send_message(&OutMsg::Next);
}
}
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
self.body_rect,
self.style.rounding,
self.style.body.color,
);
let placeholder_art_color = THEME.palette.overlay;
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, placeholder_art_color);
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM,
self.footer_rect,
self.style.rounding,
self.style.footer.color,
);
}
}
impl MusicPlayerWidget {
pub fn new(panel: Panel) -> Self {
let style = MusicPlayerStyle::default();
let display_font = Font::new(crate::DISPLAY_FONT);
let content_font = Font::new(crate::CONTENT_FONT);
let make_body_label = |content: &str| {
let text = LabelText {
font: display_font,
text: content.to_string(),
};
let color = style.body.text_color;
let label = Label::new_centered(text, 10.0, color);
Offset::new(label, Vec2::ZERO)
};
let make_footer_label = |content: &str| {
let text = LabelText {
font: content_font,
text: content.to_string(),
};
let size = style.footer.height;
let scale = size * 0.4;
let baseline = size * 0.125;
let label = Label::new(
text,
HorizontalAlignment::Center,
scale,
THEME.palette.text,
0.0,
0.0,
baseline,
);
Offset::new(label, Vec2::ZERO)
};
let icon_font = Font::new(crate::ICON_FONT);
let prev_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let play_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let next_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let primary_button = RoundButtonStyle {
radius: style.footer.height * 0.3,
spacing: style.footer.height * 0.1,
thickness: style.footer.height * 0.025,
body_color: THEME.palette.yellow,
ring_color: THEME.palette.yellow,
icon_color: THEME.palette.black,
};
let secondary_button = RoundButtonStyle {
radius: style.footer.height * 0.25,
spacing: style.footer.height * 0.05,
thickness: style.footer.height * 0.025,
body_color: style.footer.color,
ring_color: THEME.palette.black,
icon_color: THEME.palette.black,
};
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
let play = RoundButton::new(primary_button, Some(play_text));
let next = RoundButton::new(secondary_button, Some(next_text));
let slider = Slider::new(
Default::default(),
Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
);
Self {
panel,
artist: make_body_label("Artist"),
album: make_body_label("Album"),
track: make_body_label("Track"),
previous: Offset::new(prev, Vec2::ZERO),
play: Offset::new(play, Vec2::ZERO),
next: Offset::new(next, Vec2::ZERO),
position: make_footer_label("--:--"),
duration: make_footer_label("--:--"),
slider,
style,
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
position_secs: 0.0,
duration_secs: 0.0,
position_dirty: false,
position_updating: false,
status: PlaybackStatus::Paused,
}
}
pub fn send_message(&self, msg: &OutMsg) {
let msg = serde_json::to_vec(msg).unwrap();
self.panel.send_message(&msg);
}
pub fn format_time(secs: f32) -> String {
let duration = secs.floor() as usize;
let seconds = duration % 60;
let minutes = (duration / 60) % 60;
let hours = (duration / 60) / 60;
if hours > 0 {
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{:02}:{:02}", minutes, seconds)
}
}
pub fn resize(&mut self, new_size: Vec2) {
let style = &self.style;
let width = new_size.x;
let body_height = new_size.y - style.footer.height;
let body_height = body_height.max(0.0);
let body_size = Vec2::new(width, body_height);
let footer_size = Vec2::new(width, style.footer.height);
let art_size = body_height - style.art_margin * 2.0;
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
let label_x = (width + self.art_rect.br.x) / 2.0;
let artist_baseline = body_height * 0.25;
let album_baseline = body_height * 0.55;
let track_baseline = body_height * 0.85;
let button_y = body_height + style.footer.height / 2.0;
let previous_x = style.button_spacing * 0.5;
let play_x = style.button_spacing * 1.5;
let next_x = style.button_spacing * 2.5;
let position_x = style.button_spacing * 3.5;
let slider_left = style.button_spacing * 4.25;
let slider_right = width - style.button_spacing * 1.5;
let slider_top = button_y - style.slider_height / 2.0;
let slider_bottom = button_y + style.slider_height / 2.0;
let duration_x = width - style.button_spacing * 0.75;
let slider_rect = Rect {
tl: Vec2::new(slider_left, slider_top),
br: Vec2::new(slider_right, slider_bottom),
};
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
self.album.set_offset(Vec2::new(label_x, album_baseline));
self.track.set_offset(Vec2::new(label_x, track_baseline));
self.position.set_offset(Vec2::new(position_x, button_y));
self.duration.set_offset(Vec2::new(duration_x, button_y));
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
self.previous.set_offset(Vec2::new(previous_x, button_y));
self.play.set_offset(Vec2::new(play_x, button_y));
self.next.set_offset(Vec2::new(next_x, button_y));
self.slider.set_rect(slider_rect);
}
pub fn update_album(&mut self, info: AlbumInfo) {
self.album.set_text(
info.title
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<album here>"),
);
}
pub fn update_track(&mut self, info: TrackInfo) {
self.artist.set_text(
info.artists
.first()
.map(|s| s.as_str())
.unwrap_or("<artist here>"),
);
self.track.set_text(
info.title
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<album here>"),
);
if let Some(length) = info.length {
self.duration.set_text(&Self::format_time(length));
self.duration_secs = length;
} else {
self.duration.set_text("--:--");
}
}
pub fn update_progress(&mut self, progress: ProgressChanged) {
self.position_secs = progress.position;
self.position_dirty = true;
}
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
self.status = status;
let icon = match status {
PlaybackStatus::Playing => "",
PlaybackStatus::Paused => "",
PlaybackStatus::Stopped => "",
};
self.play.set_text(icon);
}
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::widgets::prelude::*;
use api::*;
use canary_script::*;
use canary_textwrap::{Content, Layout, TextCache};
use dialog::{DialogBodyStyle, DialogHeaderStyle};
use shell::Offset;
use text::{Label, LabelText};
pub struct NotificationStyle {
pub header: DialogHeaderStyle,
pub body: DialogBodyStyle,
pub rounding: f32,
}
impl Default for NotificationStyle {
fn default() -> Self {
Self {
header: DialogHeaderStyle {
height: 12.0,
..Default::default()
},
body: Default::default(),
rounding: THEME.metrics.surface_rounding,
}
}
}
pub struct NotificationPanel {
panel: Panel,
style: NotificationStyle,
summary: Label,
text_cache: TextCache,
body: Content,
body_layout: Layout,
header_rect: Rect,
body_rect: Rect,
}
impl PanelImpl for NotificationPanel {
fn update(&mut self, dt: f32) {}
fn draw(&mut self) {
let ctx = DrawContext::new(self.panel);
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
self.header_rect,
self.style.rounding,
self.style.header.color,
);
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM,
self.body_rect,
self.style.rounding,
self.style.body.color,
);
self.summary.draw(&ctx);
let ctx = ctx.with_offset(Vec2::new(5.0, 20.0));
self.body_layout
.draw(&self.text_cache, &ctx, 5.0, 8.0, THEME.palette.text);
}
fn on_resize(&mut self, new_size: Vec2) {
let style = &self.style;
let width = new_size.x;
let body_height = new_size.y - style.header.height;
let body_height = body_height.max(0.0);
let header_size = Vec2::new(width, style.header.height);
let body_size = Vec2::new(width, body_height);
self.header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
self.body_rect = Rect::from_xy_size(self.header_rect.bl(), body_size);
let width = (new_size.x - 10.0) / 5.0;
self.body_layout = self.body.layout(&mut self.text_cache, width);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
fn on_message(&mut self, msg: Message) {}
}
impl NotificationPanel {
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let msg = msg.to_vec();
let msg: canary_notifications::Contents = serde_json::from_slice(&msg).unwrap();
let style = NotificationStyle::default();
let font = style.header.text_font;
let text = msg.summary;
let text = LabelText { font, text };
let scale = style.header.height * style.header.text_scale_factor;
let summary = Label::new(
text,
text::HorizontalAlignment::Left,
scale,
style.header.text_color,
5.0,
5.0,
style.header.height * (1.0 - style.header.text_baseline),
);
let font = Font::new(crate::CONTENT_FONT);
let text = msg.body.unwrap_or(String::new());
let mut text_cache = TextCache::default();
let body = Content::from_plain(&mut text_cache, font, &text);
let body_layout = body.layout(&text_cache, 0.0);
let header_rect = Default::default();
let body_rect = Default::default();
Box::new(Self {
style,
panel,
summary,
text_cache,
body,
body_layout,
header_rect,
body_rect,
})
}
}

View File

@ -1 +0,0 @@

173
scripts/sao-ui/src/style.rs Normal file
View File

@ -0,0 +1,173 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary_script::Color;
/// A reusable set of colors. Used by default widget styles.
pub struct Palette {
pub base: Color,
pub base_hover: Color,
pub base_active: Color,
pub surface: Color,
pub overlay: Color,
pub text: Color,
pub black: Color,
pub red: Color,
pub green: Color,
pub yellow: Color,
pub blue: Color,
pub magenta: Color,
pub cyan: Color,
pub white: Color,
}
impl Palette {
pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> {
vec![
("Base", self.base),
("Base Hover", self.base_hover),
("Base Active", self.base_active),
("Surface", self.surface),
("Overlay", self.overlay),
("Text", self.text),
("Black", self.black),
("Red", self.red),
("Green", self.green),
("Yellow", self.yellow),
("Blue", self.blue),
("Magenta", self.magenta),
("Cyan", self.cyan),
("White", self.white),
]
}
}
/// The common base color alpha shared between all themes.
pub const BASE_ALPHA: u8 = 230;
/// The common base_hover color alpha shared between all themes.
pub const BASE_HOVER_ALPHA: u8 = 242;
/// Converts 0xrrggbb hex to an opaque [Color].
pub const fn hex(rgb: u32) -> Color {
Color((rgb << 8) | 0xff)
}
/// Sword Art Online color palette.
pub const SAO_PALETTE: Palette = Palette {
base: Color::WHITE.with_alpha(BASE_ALPHA),
base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA),
base_active: Color::YELLOW,
surface: Color::WHITE,
overlay: Color::WHITE,
text: Color::BLACK,
black: Color::BLACK,
red: Color::RED,
green: Color::GREEN,
yellow: Color::YELLOW,
blue: Color::BLUE,
magenta: Color::MAGENTA,
cyan: Color::CYAN,
white: Color::WHITE,
};
/// Rose Pine color palette.
pub const ROSE_PINE_PALETTE: Palette = Palette {
base: hex(0x191724).with_alpha(BASE_ALPHA),
base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x403d52), // Highlight Med
surface: hex(0x1f1d2e),
overlay: hex(0x26233a),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x31748f), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// Rose Pine Moon color palette.
pub const ROSE_PINE_MOON_PALETTE: Palette = Palette {
base: hex(0x232136).with_alpha(BASE_ALPHA),
base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x44415a), // Highlight Med
surface: hex(0x2a273f),
overlay: hex(0x393552),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x3e8fb0), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme.
pub const ARCTICA: [Color; 24] = [
hex(0x242933),
hex(0x2e3440),
hex(0x3b4252),
hex(0x4c566a),
hex(0xeceff4),
hex(0xd8dee9),
hex(0xc2c9d6),
hex(0xaeb7c6),
hex(0xa8555d),
hex(0xb77763),
hex(0xcdb179),
hex(0x8ba277),
hex(0x769b9b),
hex(0x72a1ae),
hex(0x5e81ac),
hex(0x92738c),
hex(0xbf616a),
hex(0xd08770),
hex(0xebcb8b),
hex(0xa3be8c),
hex(0x8fbcbb),
hex(0x88c0d0),
hex(0x81a1c1),
hex(0xb48ead),
];
/// [Arctica](https://github.com/sashakoshka/arctica) color palette.
pub const ARCTICA_PALETTE: Palette = Palette {
base: ARCTICA[0].with_alpha(BASE_ALPHA),
base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA),
base_active: ARCTICA[13],
surface: ARCTICA[2],
overlay: ARCTICA[3],
text: ARCTICA[5],
black: ARCTICA[3],
red: ARCTICA[8],
green: ARCTICA[11],
yellow: ARCTICA[10],
blue: ARCTICA[14],
magenta: ARCTICA[15],
cyan: ARCTICA[13],
white: ARCTICA[7],
};
/// Common measurements for widget shapes.
pub struct Metrics {
pub surface_rounding: f32,
}
/// Common default parameters for widget styles.
pub struct Theme {
pub palette: Palette,
pub metrics: Metrics,
}
/// The global theme.
pub const THEME: Theme = Theme {
palette: ROSE_PINE_MOON_PALETTE,
metrics: Metrics {
surface_rounding: 5.0,
},
};

View File

@ -48,6 +48,12 @@ impl RoundButton {
icon,
}
}
pub fn set_text(&mut self, text: &str) {
if let Some(icon) = self.icon.as_mut() {
icon.set_text(text);
}
}
}
impl Button for RoundButton {
@ -112,6 +118,8 @@ pub struct RectButtonStyle {
pub inactive_color: Color,
pub hover_color: Color,
pub selected_color: Color,
pub icon_color: Color,
pub label_color: Color,
}
impl Default for RectButtonStyle {
@ -123,9 +131,11 @@ impl Default for RectButtonStyle {
label_baseline: 0.25,
icon_scale_factor: 0.8,
icon_margin_factor: 1.1,
inactive_color: Color::WHITE.with_alpha(0x40),
hover_color: Color::WHITE.with_alpha(0xb0),
selected_color: Color::YELLOW,
inactive_color: THEME.palette.base,
hover_color: THEME.palette.base_hover,
selected_color: THEME.palette.base_active,
icon_color: THEME.palette.black,
label_color: THEME.palette.text,
}
}
}
@ -154,7 +164,7 @@ impl RectButton {
label: Option<LabelText>,
icon: Option<LabelText>,
) -> Self {
let mut label_left = rect.bl.x;
let mut label_left = rect.tl.x;
let mut alignment = HorizontalAlignment::Center;
let icon = icon.map(|text| {
@ -162,9 +172,9 @@ impl RectButton {
label_left += margin;
alignment = HorizontalAlignment::Left;
let scale = rect.height() * style.icon_scale_factor;
let color = Color::BLACK;
let cx = rect.bl.x + margin / 2.0;
let cy = rect.bl.y + rect.height() / 2.0;
let color = style.icon_color;
let cx = rect.tl.x + margin / 2.0;
let cy = rect.tl.y + rect.height() / 2.0;
let center = Vec2::new(cx, cy);
Icon::new(text, scale, color, center)
@ -173,10 +183,10 @@ impl RectButton {
let label = label.map(|text| {
let scale = rect.height() * style.label_scale_factor;
let left = label_left;
let right = rect.tr.x;
let baseline = rect.bl.y;
let baseline = (rect.height() * style.label_baseline) + baseline;
let color = Color::BLACK;
let right = rect.br.x;
let baseline = rect.tl.y;
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
let color = style.label_color;
Label::new(text, alignment, scale, color, left, right, baseline)
});

View File

@ -23,15 +23,14 @@ impl DialogResponse {
pub fn get_color(&self) -> Color {
match self {
DialogResponse::Yes => Color::BLUE,
DialogResponse::No => Color::RED,
DialogResponse::Yes => THEME.palette.blue,
DialogResponse::No => THEME.palette.red,
}
}
}
#[derive(Clone)]
pub struct DialogStyle {
pub width: f32,
pub rounding: f32,
pub header: DialogHeaderStyle,
pub body: DialogBodyStyle,
@ -41,8 +40,7 @@ pub struct DialogStyle {
impl Default for DialogStyle {
fn default() -> Self {
Self {
width: 1.2,
rounding: 0.02,
rounding: THEME.metrics.surface_rounding,
header: Default::default(),
body: Default::default(),
footer: Default::default(),
@ -63,10 +61,10 @@ pub struct DialogHeaderStyle {
impl Default for DialogHeaderStyle {
fn default() -> Self {
Self {
color: Color::WHITE,
height: 0.3,
color: THEME.palette.surface,
height: 20.0,
text_font: Font::new(crate::DISPLAY_FONT),
text_color: Color::BLACK,
text_color: THEME.palette.text,
text_scale_factor: 0.65,
text_baseline: 0.25,
}
@ -76,20 +74,18 @@ impl Default for DialogHeaderStyle {
#[derive(Clone)]
pub struct DialogBodyStyle {
pub color: Color,
pub height: f32,
pub text_font: Font,
pub text_color: Color,
pub text_scale_factor: f32,
pub text_size: f32,
}
impl Default for DialogBodyStyle {
fn default() -> Self {
Self {
color: Color::WHITE.with_alpha(0xb0),
height: 0.6,
color: THEME.palette.base,
text_font: Font::new(crate::CONTENT_FONT),
text_color: Color::BLACK,
text_scale_factor: 0.15,
text_color: THEME.palette.text,
text_size: 5.0,
}
}
}
@ -99,6 +95,7 @@ pub struct DialogFooterStyle {
pub icon_font: Font,
pub button_radius: f32,
pub color: Color,
pub button_fg: Color,
pub height: f32,
}
@ -106,9 +103,10 @@ impl Default for DialogFooterStyle {
fn default() -> Self {
Self {
icon_font: Font::new(crate::ICON_FONT),
button_radius: 0.1,
color: Color::WHITE,
height: 0.25,
button_radius: 7.5,
color: THEME.palette.surface,
button_fg: THEME.palette.white,
height: 15.0,
}
}
}
@ -122,53 +120,16 @@ pub struct DialogInfo {
pub struct Dialog {
style: DialogStyle,
title: Label,
content: Label,
title: Offset<Label>,
content: Offset<Label>,
content_size: Vec2,
buttons: Vec<DialogButton>,
}
impl Dialog {
pub fn new(style: DialogStyle, info: &DialogInfo) -> Self {
let width2 = style.width / 2.0;
let button_y = -(style.body.height + style.footer.height) / 2.0;
let button_spacing = style.width / info.responses.len() as f32;
let button_spacing2 = button_spacing / 2.0;
let title_scale = style.header.height * style.header.text_scale_factor;
let title_baseline =
style.header.height * style.header.text_baseline + style.body.height / 2.0;
let title = Label::new(
LabelText {
font: style.header.text_font,
text: info.title.to_string(),
},
HorizontalAlignment::Center,
title_scale,
style.header.text_color,
-width2,
width2,
title_baseline,
);
let content_scale = style.body.height * style.body.text_scale_factor;
let content = Label::new(
LabelText {
font: style.body.text_font,
text: info.content.to_string(),
},
HorizontalAlignment::Center,
content_scale,
style.body.text_color,
-width2,
width2,
0.0,
);
let mut buttons = Vec::new();
for (index, response) in info.responses.iter().enumerate() {
let button_x = button_spacing * index as f32 + button_spacing2 - style.width / 2.0;
for response in info.responses.iter() {
let color = response.get_color();
let radius = style.footer.button_radius;
let button_style = RoundButtonStyle {
@ -177,7 +138,7 @@ impl Dialog {
thickness: radius * 0.05,
body_color: color,
ring_color: color,
icon_color: Color::WHITE,
icon_color: style.footer.button_fg,
};
let text = LabelText {
@ -186,7 +147,7 @@ impl Dialog {
};
let button = RoundButton::new(button_style, Some(text));
let button = Offset::new(button, Vec2::new(button_x, button_y));
let button = Offset::new(button, Vec2::ZERO);
buttons.push(DialogButton {
response: *response,
@ -194,11 +155,67 @@ impl Dialog {
});
}
Self {
let title_scale = style.header.height * style.header.text_scale_factor;
let title = Label::new(
LabelText {
font: style.header.text_font,
text: info.title.to_string(),
},
HorizontalAlignment::Center,
title_scale,
style.header.text_color,
0.0,
0.0,
0.0,
);
let content = Label::new(
LabelText {
font: style.body.text_font,
text: info.content.to_string(),
},
HorizontalAlignment::Center,
style.body.text_size,
style.body.text_color,
0.0,
0.0,
0.0,
);
let mut dialog = Self {
style,
title,
content,
title: Offset::new(title, Vec2::ZERO),
content: Offset::new(content, Vec2::ZERO),
content_size: Vec2::ONE,
buttons,
};
dialog.resize(Vec2::splat(100.0));
dialog
}
pub fn resize(&mut self, size: Vec2) {
let style = &self.style;
let width = size.x;
let width2 = width / 2.0;
let body_height = size.y - style.header.height - style.footer.height;
let body_height = body_height.max(0.0);
let title_baseline = style.header.height * (1.0 - style.header.text_baseline);
let content_baseline = style.header.height + body_height / 2.0;
let button_baseline = style.header.height + body_height + style.footer.height / 2.0;
let button_spacing = width / self.buttons.len() as f32;
let button_spacing2 = button_spacing / 2.0;
self.content_size = Vec2::new(width, body_height);
self.title.set_offset(Vec2::new(width2, title_baseline));
self.content.set_offset(Vec2::new(width2, content_baseline));
for (index, button) in self.buttons.iter_mut().enumerate() {
let button_x = button_spacing * index as f32 + button_spacing2;
button
.button
.set_offset(Vec2::new(button_x, button_baseline));
}
}
}
@ -215,24 +232,15 @@ impl Container for Dialog {
fn draw(&mut self, ctx: &DrawContext) {
let style = &self.style;
let width = style.width;
let width2 = width / 2.0;
let width = self.content_size.x;
let rounding = style.rounding;
let header_xy = Vec2::new(-width2, style.body.height / 2.0);
let header_size = Vec2::new(width, style.header.height);
let header_rect = Rect::from_xy_size(header_xy, header_size);
let header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
let header_corners = CornerFlags::TOP;
let body_tr = Vec2::new(width2, style.body.height / 2.0);
let body_rect = Rect {
bl: -body_tr,
tr: body_tr,
};
let footer_xy = Vec2::new(-width2, -style.body.height / 2.0 - style.footer.height);
let body_rect = Rect::from_xy_size(header_rect.bl(), self.content_size);
let footer_size = Vec2::new(width, style.footer.height);
let footer_rect = Rect::from_xy_size(footer_xy, footer_size);
let footer_rect = Rect::from_xy_size(body_rect.bl(), footer_size);
let footer_corners = CornerFlags::BOTTOM;
ctx.draw_rect(body_rect, style.body.color);

View File

@ -52,7 +52,7 @@ impl<T> SlotMenu<T> {
let out_delay = i as f32 * inter_button_delay;
let in_delay = max_delay - out_delay;
let mut slide_anim = Animation::new(EaseOut, duration, 0.25, 0.0);
let mut slide_anim = Animation::new(EaseOut, duration, -50.0, 0.0);
slide_anim.set_in_delay(in_delay);
slide_anim.set_out_delay(out_delay);
slide_anim.ease_in();
@ -108,7 +108,7 @@ impl<T> SlotMenu<T> {
pub fn for_buttons(&mut self, mut cb: impl FnMut(&mut SlotMenuButton<T>, usize, f32)) {
for (i, button) in self.buttons.iter_mut().enumerate() {
let y = -(i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
let y = (i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
cb(button, i, y);
}
}
@ -222,22 +222,23 @@ pub struct TabMenu {
}
impl TabMenu {
const HEAD_RADIUS: f32 = 0.05;
const HEAD_HEIGHT: f32 = 0.1;
const TAB_WIDTH: f32 = 0.1;
const TAB_HEIGHT: f32 = 0.15;
const HEAD_RADIUS: f32 = 5.0;
const HEAD_HEIGHT: f32 = 15.0;
const HEAD_COLOR: Color = THEME.palette.surface;
const TAB_WIDTH: f32 = 15.0;
const TAB_HEIGHT: f32 = 25.0;
const TAB_NUM: usize = 6;
const SEPARATOR_WIDTH: f32 = 0.02;
const INNER_RADIUS: f32 = 0.01;
const CONTENT_WIDTH: f32 = 0.64;
const SEPARATOR_WIDTH: f32 = 5.0;
const INNER_RADIUS: f32 = 5.0;
const CONTENT_WIDTH: f32 = 100.0;
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
radius: Self::HEAD_RADIUS * 0.5,
spacing: Self::HEAD_RADIUS * 0.2,
thickness: Self::HEAD_RADIUS * 0.1,
body_color: Color::WHITE,
ring_color: Color::BLACK,
icon_color: Color::BLACK,
radius: Self::HEAD_HEIGHT * 0.25,
spacing: Self::HEAD_HEIGHT * 0.1,
thickness: Self::HEAD_HEIGHT * 0.05,
body_color: Self::HEAD_COLOR,
ring_color: THEME.palette.black,
icon_color: THEME.palette.black,
};
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
@ -248,8 +249,8 @@ impl TabMenu {
let mut tabs = Vec::new();
for i in 0..Self::TAB_NUM {
let y = (i + 1) as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, -y);
let y = i as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, y);
let mut style = RectButtonStyle::default();
style.radius = Self::HEAD_RADIUS;
@ -280,7 +281,7 @@ impl TabMenu {
}),
);
let head_button_y = Self::HEAD_HEIGHT / 2.0;
let head_button_y = -Self::HEAD_HEIGHT / 2.0;
let pop_out_x = Self::HEAD_BUTTON_MARGIN;
let pop_out = Offset::new(pop_out, Vec2::new(pop_out_x, head_button_y));
@ -295,8 +296,8 @@ impl TabMenu {
let scroll_bar = Offset::new(scroll_bar, Vec2::new(scroll_x, -tab_list_height));
let separator_rect = Rect {
bl: Vec2::new(Self::TAB_WIDTH, -tab_list_height),
tr: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
tl: Vec2::new(Self::TAB_WIDTH, 0.0),
br: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, tab_list_height),
};
let head_width = Self::TAB_WIDTH
@ -306,13 +307,13 @@ impl TabMenu {
+ scroll_bar.style.margin.x * 2.0;
let head_rect = Rect {
bl: Vec2::ZERO,
tr: Vec2::new(head_width, Self::HEAD_HEIGHT),
tl: Vec2::new(0.0, -Self::HEAD_HEIGHT),
br: Vec2::new(head_width, 0.0),
};
let view_rect = Rect {
bl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, -tab_list_height),
tr: Vec2::new(head_rect.tr.x, 0.0),
tl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
br: Vec2::new(head_rect.br.x, tab_list_height),
};
let view = ScrollView::new(
@ -322,7 +323,7 @@ impl TabMenu {
|available_width: f32| Inventory::new(available_width),
);
let view = Offset::new(view, view_rect.bl);
let view = Offset::new(view, view_rect.tl);
Self {
pop_out,
@ -348,20 +349,18 @@ impl Container for TabMenu {
}
fn draw(&mut self, ctx: &DrawContext) {
let head_color = Color::WHITE;
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM_RIGHT,
self.separator_rect,
Self::INNER_RADIUS,
head_color,
Self::HEAD_COLOR,
);
ctx.draw_partially_rounded_rect(
CornerFlags::TOP_LEFT | CornerFlags::TOP_RIGHT,
CornerFlags::TOP,
self.head_rect,
Self::HEAD_RADIUS,
head_color,
Self::HEAD_COLOR,
);
}
}

View File

@ -8,7 +8,9 @@ pub mod button;
pub mod dialog;
pub mod flex;
pub mod menu;
pub mod palette;
pub mod scroll;
pub mod slider;
pub mod shell;
pub mod text;
@ -75,6 +77,7 @@ impl<T: Container> Widget for T {
pub mod prelude {
pub use super::*;
pub use crate::anim::Animation;
pub use crate::style::{self, THEME};
pub use canary_script::{*, api::*};
pub use keyframe::functions::*;
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::prelude::*;
use shell::Offset;
use text::{HorizontalAlignment, Label, LabelText};
pub struct PaletteStyle {
pub bg: Color,
pub text: Color,
pub rounding: f32,
pub text_size: f32,
pub line_spacing: f32,
pub color_radius: f32,
pub margin: Rect,
}
impl Default for PaletteStyle {
fn default() -> Self {
Self {
bg: THEME.palette.surface,
text: THEME.palette.text,
rounding: THEME.metrics.surface_rounding,
text_size: 5.0,
line_spacing: 8.0,
color_radius: 3.0,
margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO),
}
}
}
/// A widget that displays all the colors in the global palette.
pub struct Palette {
body: Rect,
style: PaletteStyle,
labels: Vec<Offset<Label>>,
colors: Vec<(Vec2, Color)>,
}
impl Palette {
pub fn new(style: PaletteStyle) -> Self {
let width = 70.0;
let pairs = THEME.palette.make_label_pairs();
let label_font = Font::new(crate::CONTENT_FONT);
let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl;
let mut color_cursor = Vec2::new(
width - style.margin.br.x,
style.line_spacing / 2.0 + style.margin.tl.y,
);
let mut labels = Vec::new();
let mut colors = Vec::new();
for (text, color) in pairs {
let text = LabelText {
font: label_font,
text: text.to_string(),
};
let label = Label::new(
text,
HorizontalAlignment::Left,
style.text_size,
style.text,
0.0,
0.0,
0.0,
);
let label = Offset::new(label, label_cursor);
labels.push(label);
colors.push((color_cursor, color));
label_cursor.y += style.line_spacing;
color_cursor.y += style.line_spacing;
}
let height = label_cursor.y + style.margin.br.y;
Self {
body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)),
style,
labels,
colors,
}
}
}
impl RectBounds for Palette {
fn get_bounds(&self) -> Rect {
self.body
}
}
impl Widget for Palette {
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg);
for label in self.labels.iter_mut() {
label.draw(ctx);
}
for (center, color) in self.colors.iter() {
ctx.draw_circle(*center, self.style.color_radius, *color);
}
}
}

View File

@ -18,14 +18,14 @@ pub struct ScrollBarStyle {
impl Default for ScrollBarStyle {
fn default() -> Self {
Self {
margin: Vec2::new(0.01, 0.01),
body_radius: 0.005,
body_width: 0.015,
body_idle_color: Color(0x7f7f7fff),
body_hover_color: Color(0xb0b0b0ff),
body_selected_color: Color::MAGENTA,
rail_width: 0.005,
rail_color: Color(0xa0a0a07f),
margin: Vec2::splat(2.0),
body_radius: 1.0,
body_width: 3.0,
body_idle_color: THEME.palette.base,
body_hover_color: THEME.palette.base_hover,
body_selected_color: THEME.palette.base_active,
rail_width: 1.0,
rail_color: THEME.palette.base,
}
}
}
@ -50,8 +50,8 @@ impl ScrollBar {
pub fn new(height: f32, content_height: f32, style: ScrollBarStyle) -> Self {
let center_x = style.body_width / 2.0 + style.margin.x;
let rail_rect = Rect {
bl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
tr: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
tl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
br: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
};
let body_color_anim = Animation::new(
@ -80,7 +80,7 @@ impl ScrollBar {
let style = &self.style;
let rail_height = self.rail_rect.height();
let body_height = (self.height / self.content_height) * rail_height;
let body_y = rail_height - (self.scroll / self.content_height) * rail_height - body_height;
let body_y = (self.scroll / self.content_height) * rail_height;
let body_xy = Vec2::new(style.margin.x, body_y + style.margin.y);
let body_size = Vec2::new(style.body_width, body_height);
Rect::from_xy_size(body_xy, body_size)
@ -128,7 +128,7 @@ impl Widget for ScrollBar {
}
if kind == CursorEventKind::Drag && self.is_selected {
self.scroll = ((self.grab_coord - at.y) / self.rail_rect.height())
self.scroll = ((at.y - self.grab_coord) / self.rail_rect.height())
* self.content_height
+ self.grab_scroll;
@ -203,7 +203,7 @@ impl<T: Widget> ScrollView<T> {
impl<T: Widget> Widget for ScrollView<T> {
fn update(&mut self, dt: f32) {
if self.scroll_bar.is_dirty() {
let yoff = self.scroll_bar.get_scroll() - self.content_height + self.height;
let yoff = -self.scroll_bar.get_scroll();
self.inner.set_offset(Vec2::new(0.0, yoff));
}

View File

@ -132,8 +132,8 @@ impl<T: RectBounds> Offset<T> {
vert_align: OffsetAlignment,
) -> Self {
let bounds = inner.get_bounds();
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
let x = hori_align.align(bounds.tl.x, bounds.br.x);
let y = vert_align.align(bounds.br.y, bounds.tl.y);
let offset = anchor - Vec2::new(x, y);
Self { inner, offset }
}
@ -180,8 +180,8 @@ impl<T: RectBounds> Popup<T> {
vert_align: OffsetAlignment,
) -> Self {
let bounds = inner.get_bounds();
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
let x = hori_align.align(bounds.tl.x, bounds.br.x);
let y = vert_align.align(bounds.br.y, bounds.tl.y);
let offset = anchor - Vec2::new(x, y);
Self { inner, offset }
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::prelude::*;
pub struct SliderStyle {
pub bg_color: Color,
pub bg_padding: f32,
pub bg_rounding: f32,
pub fg_color: Color,
}
impl Default for SliderStyle {
fn default() -> Self {
Self {
bg_color: THEME.palette.overlay,
bg_padding: 2.5,
bg_rounding: 2.5,
fg_color: THEME.palette.blue,
}
}
}
pub struct Slider {
style: SliderStyle,
bg_rect: Rect,
fg_rect: Rect,
position: f32,
dirty: bool,
updating: bool,
}
impl Slider {
pub fn new(style: SliderStyle, rect: Rect) -> Self {
Self {
style,
bg_rect: rect,
fg_rect: rect,
position: 0.5,
dirty: true,
updating: false,
}
}
pub fn set_position(&mut self, position: f32) {
if !self.updating {
self.position = position;
self.dirty = true;
}
}
pub fn has_update(&mut self) -> Option<f32> {
if self.updating {
Some(self.position)
} else {
None
}
}
pub fn set_rect(&mut self, rect: Rect) {
self.bg_rect = rect;
self.dirty = true;
}
fn undirty(&mut self) {
if !self.dirty {
return;
}
let mut fg_space = self.bg_rect.inset(self.style.bg_padding);
fg_space.br.x = (fg_space.width() * self.position) + fg_space.tl.x;
self.fg_rect = fg_space;
self.dirty = false;
}
pub fn get_position(&self) -> f32 {
self.position
}
}
impl Widget for Slider {
fn draw(&mut self, ctx: &DrawContext) {
self.undirty();
ctx.draw_rounded_rect(self.bg_rect, self.style.bg_rounding, self.style.bg_color);
ctx.draw_rect(self.fg_rect, self.style.fg_color);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if let CursorEventKind::Select = kind {
if self.bg_rect.contains_point(at) {
self.updating = true;
}
}
if !self.updating {
return;
}
match kind {
CursorEventKind::Hover => {}
CursorEventKind::Select | CursorEventKind::Drag => {
let offset = at.x - self.fg_rect.tl.x;
let range = self.bg_rect.inset(self.style.bg_padding).width();
self.position = (offset / range).clamp(0.0, 1.0);
self.dirty = true;
}
CursorEventKind::Deselect => {
self.updating = false;
}
}
}
}

View File

@ -53,6 +53,26 @@ impl Label {
offset: Vec2::ZERO,
}
}
pub fn new_centered(text: LabelText, scale: f32, color: Color) -> Self {
Self::new(
text,
HorizontalAlignment::Center,
scale,
color,
0.0,
0.0,
0.0,
)
}
pub fn set_text(&mut self, text: &str) {
if self.text.text != text {
self.text.text = text.to_string();
self.layout = None;
self.dirty = true;
}
}
}
impl Widget for Label {
@ -62,14 +82,14 @@ impl Widget for Label {
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
self.bounds = bounds;
let xoff = match self.alignment {
HorizontalAlignment::Left => self.left - bounds.bl.x,
HorizontalAlignment::Right => self.right - bounds.tr.x,
HorizontalAlignment::Left => self.left - bounds.tl.x,
HorizontalAlignment::Right => self.right - bounds.br.x,
HorizontalAlignment::Center => {
let available = self.right - self.left;
let halfway = available / 2.0 + self.left;
let width = bounds.tr.x - bounds.bl.x;
let width = bounds.br.x - bounds.tl.x;
let left = halfway - width / 2.0;
left - bounds.bl.x
left - bounds.tl.x
}
};
@ -108,6 +128,14 @@ impl Icon {
offset: Vec2::ZERO,
}
}
pub fn set_text(&mut self, text: &str) {
if self.text.text != text {
self.text.text = text.to_string();
self.layout = None;
self.dirty = true;
}
}
}
impl Widget for Icon {
@ -117,7 +145,7 @@ impl Widget for Icon {
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
self.bounds = bounds;
self.offset = self.center - bounds.size() / 2.0;
self.offset.y -= bounds.bl.y;
self.offset.y -= bounds.tl.y;
self.dirty = false;
self.layout = Some(layout);
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later
//! This module defines backends for WebAssembly execution.
//!
@ -8,6 +8,8 @@
//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3)
//! will also be provided.
use std::collections::VecDeque;
use super::*;
pub mod wasmtime;
@ -17,12 +19,14 @@ pub mod wasmtime;
/// Currently, only ever creates [wasmtime::WasmtimeBackend].
pub fn make_default_backend() -> anyhow::Result<Box<dyn Backend>> {
let backend = wasmtime::WasmtimeBackend::new()?;
log::info!("Created default ({}) backend", backend.name());
Ok(Box::new(backend))
}
/// A WebAssembly runtime backend.
pub trait Backend {
fn load_module(&self, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
fn name(&self) -> &'static str;
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
}
/// An instance of a WebAssembly module.
@ -36,10 +40,11 @@ pub trait Instance {
/// Binds script data to a Canary panel.
///
/// To "bind" a Canary panel to a Canary script, this function must be
/// called. It passes the ID of a panel to the script, plus an
/// initialization message, and the script returns an integer as
/// userdata. All panel events will be identified to the script with this
/// userdata as the first argument.
/// called. It passes the ID of a panel to the script, the name of the
/// protocol that this panel will be using, plus an initialization
/// message, and the script returns an integer as userdata. All panel
/// events will be identified to the script with this userdata as the first
/// argument.
///
/// The intended usecase for this userdata is to contain a pointer. A
/// Canary script can allocate some high-level object in memory, and when
@ -47,7 +52,7 @@ pub trait Instance {
/// userdata. Then, when the runtime calls back into the script, the
/// userdata will be reinterpreted as a pointer and a method can be called
/// on that object in memory.
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32;
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32;
fn update(&self, panel_ud: u32, dt: f32);
@ -59,3 +64,128 @@ pub trait Instance {
fn on_message(&self, panel_ud: u32, msg: Vec<u8>);
}
#[derive(Default)]
pub struct ScriptAbi {
draw_cmds: Mutex<Vec<DrawCommand>>,
font_store: Arc<text::FontStore>,
font_families: Mutex<HashMap<String, u32>>,
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
text_layouts: RwLock<Slab<text::TextLayout>>,
message_store: RwLock<Slab<Vec<u8>>>,
panels: RwLock<Slab<PanelAbi>>,
}
impl ScriptAbi {
pub fn new(font_store: Arc<text::FontStore>) -> Self {
Self {
font_store,
..Default::default()
}
}
/// Allocates a new ID and host-side storage for a panel.
pub fn create_panel(&self) -> PanelId {
let abi = PanelAbi::default();
let id = self.panels.write().insert(abi);
PanelId(id)
}
pub fn start_draw(&self) {
let mut lock = self.draw_cmds.lock();
lock.clear();
}
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
self.draw_cmds.lock().push(DrawCommand::Mesh {
vertices: vertices.to_vec(),
indices: indices.to_vec(),
})
}
pub fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
// TODO multiple fonts per layout
let layouts = self.text_layouts.read();
let layout = layouts.get(id as usize).unwrap();
let glyphs = layout.glyphs.as_slice();
let loaded = self.loaded_fonts.read();
let font = loaded.get(layout.font_id as usize).unwrap();
let cmds = font.draw(glyphs, offset, scale, color);
self.draw_cmds.lock().extend(cmds.into_iter());
}
pub fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
f(self.draw_cmds.lock().as_slice());
}
pub fn font_load(&self, family: &str) -> u32 {
let mut family_cache = self.font_families.lock();
if let Some(cached) = family_cache.get(family) {
return *cached;
}
let font = self.font_store.load_font(family);
let mut loaded = self.loaded_fonts.write();
let id = loaded.len() as u32;
family_cache.insert(family.to_string(), id);
loaded.push(font);
id
}
pub fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
let loaded = self.loaded_fonts.read();
let font = loaded.get(font_id as usize).unwrap();
let layout = font.shape(text);
self.text_layouts.write().insert(layout) as u32
}
pub fn text_layout_delete(&self, id: u32) {
self.text_layouts.write().remove(id as usize);
}
pub fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
let _ = std::mem::replace(dst, src);
}
pub fn message_new(&self, data: Vec<u8>) -> u32 {
let mut store = self.message_store.write();
let id = store.insert(data) as u32;
id
}
pub fn message_free(&self, id: u32) {
let mut store = self.message_store.write();
store.remove(id as usize);
}
pub fn message_get_len(&self, id: u32) -> u32 {
self.message_store.read().get(id as usize).unwrap().len() as u32
}
pub fn message_get_data(&self, id: u32, dst: &mut [u8]) {
let store = self.message_store.read();
let src = store.get(id as usize).unwrap();
dst.copy_from_slice(src);
}
pub fn panel_send_message(&self, id: u32, message: Vec<u8>) {
if let Some(panel) = self.panels.read().get(id as usize) {
panel.outgoing_messages.write().push_back(message);
}
}
pub fn recv_panel_messages(&self, id: PanelId) -> Vec<Vec<u8>> {
if let Some(panel) = self.panels.read().get(id.0) {
panel.outgoing_messages.write().drain(..).collect()
} else {
Vec::new()
}
}
}
#[derive(Default)]
pub struct PanelAbi {
outgoing_messages: RwLock<VecDeque<Vec<u8>>>,
}

View File

@ -1,42 +1,79 @@
// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::hash::{Hasher, BuildHasherDefault};
use std::ops::DerefMut;
use std::time::Instant;
use super::{Arc, Backend, Instance, PanelId};
use crate::{DrawCommand, ScriptAbi, ScriptAbiImpl};
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
use crate::DrawCommand;
use canary_script::{Color, CursorEventKind, Rect, Vec2};
use parking_lot::Mutex;
use prehash::Passthru;
type Caller<'a> = wasmtime::Caller<'a, ScriptAbiImpl>;
type Store = wasmtime::Store<ScriptAbiImpl>;
type Linker = wasmtime::Linker<ScriptAbiImpl>;
type Caller<'a> = wasmtime::Caller<'a, Arc<ScriptAbi>>;
type Store = wasmtime::Store<Arc<ScriptAbi>>;
type Linker = wasmtime::Linker<Arc<ScriptAbi>>;
type ModuleCache = Mutex<HashMap<u64, wasmtime::Module, BuildHasherDefault<Passthru>>>;
pub struct WasmtimeBackend {
engine: wasmtime::Engine,
module_cache: ModuleCache,
}
impl WasmtimeBackend {
pub fn new() -> anyhow::Result<Self> {
log::info!("Creating wasmtime backend");
let mut config = wasmtime::Config::new();
config.wasm_simd(true);
config.wasm_bulk_memory(true);
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
config.cache_config_load_default()?;
let engine = wasmtime::Engine::new(&config)?;
Ok(Self { engine })
let module_cache = Default::default();
Ok(Self {
engine,
module_cache,
})
}
}
impl Backend for WasmtimeBackend {
fn load_module(&self, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
let module = wasmtime::Module::new(&self.engine, module)?;
let abi = ScriptAbiImpl::default();
fn name(&self) -> &'static str {
"wasmtime"
}
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
let start = Instant::now();
let mut hasher = DefaultHasher::new();
hasher.write(module);
let hash = hasher.finish();
let fmt_hash = format!("{:x}", hash);
log::debug!("Loading module (hash: {})", fmt_hash);
let mut cache = self.module_cache.lock();
let module = if let Some(module) = cache.get(&hash) {
log::debug!("Module load cache hit (hash: {})", fmt_hash);
module
} else {
log::debug!("Module load cache miss; building (hash: {})", fmt_hash);
let start = Instant::now();
let module = wasmtime::Module::new(&self.engine, module)?;
cache.insert(hash, module);
log::debug!("Built module in {:?} (hash: {})", start.elapsed(), fmt_hash);
cache.get(&hash).unwrap()
};
let mut store = wasmtime::Store::new(&self.engine, abi);
let mut linker = Linker::new(&self.engine);
WasmtimeInstance::link(&mut linker)?;
let instance = linker.instantiate(&mut store, &module)?;
let instance = linker.instantiate(&mut store, module)?;
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
let update = instance.get_typed_func(&mut store, "update")?;
let draw = instance.get_typed_func(&mut store, "draw")?;
@ -56,13 +93,19 @@ impl Backend for WasmtimeBackend {
let instance = Arc::new(instance);
log::debug!(
"Loaded module in {:?} (hash: {})",
start.elapsed(),
fmt_hash
);
Ok(instance)
}
}
pub struct WasmtimeInstance {
store: Mutex<Store>,
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
bind_panel: wasmtime::TypedFunc<(u32, u32, u32), u32>,
update: wasmtime::TypedFunc<(u32, f32), ()>,
draw: wasmtime::TypedFunc<u32, ()>,
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
@ -148,6 +191,15 @@ impl WasmtimeInstance {
},
)?;
linker.func_wrap(
module,
"panel_send_message",
|mut caller: Caller<'_>, id: u32, ptr: u32, len: u32| {
let message = Self::get_memory_slice_bytes(&mut caller, ptr as usize, len as usize);
caller.data().panel_send_message(id, message.to_vec())
},
)?;
Ok(())
}
@ -190,11 +242,13 @@ impl WasmtimeInstance {
}
impl Instance for WasmtimeInstance {
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32 {
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32 {
let mut store = self.store.lock();
let protocol = store.data().message_new(protocol.as_bytes().to_vec());
let msg = store.data().message_new(msg);
let args = (panel.0 as u32, msg);
let args = (panel.0 as u32, protocol, msg);
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
store.data().message_free(protocol);
store.data().message_free(msg);
data
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later
pub use canary_script::*;
use parking_lot::{Mutex, RwLock};
@ -10,24 +10,33 @@ use std::sync::Arc;
pub mod backend;
pub mod text;
use backend::{Backend, Instance};
use backend::{Backend, Instance, ScriptAbi};
use text::FontStore;
/// The main interface to Canary.
pub struct Runtime {
backend: Box<dyn Backend>,
font_store: Arc<FontStore>,
}
impl Runtime {
pub fn new(backend: Box<dyn Backend>) -> anyhow::Result<Self> {
Ok(Self { backend })
log::info!("Initializing runtime with {} backend", backend.name());
Ok(Self {
backend,
font_store: Arc::new(FontStore::new()),
})
}
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
let instance = self.backend.load_module(module)?;
let abi = ScriptAbi::new(self.font_store.to_owned());
let abi = Arc::new(abi);
let instance = self.backend.load_module(abi.to_owned(), module)?;
Ok(Script {
instance,
next_panel: 0,
abi,
})
}
}
@ -35,16 +44,16 @@ impl Runtime {
/// A loaded instance of a Canary script.
pub struct Script {
instance: Arc<dyn Instance>,
next_panel: usize,
abi: Arc<ScriptAbi>,
}
impl Script {
pub fn create_panel(&mut self, msg: Vec<u8>) -> anyhow::Result<Panel> {
let id = PanelId(self.next_panel);
self.next_panel += 1;
let userdata = self.instance.bind_panel(id, msg);
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
let id = self.abi.create_panel();
let userdata = self.instance.bind_panel(id, protocol, msg);
Ok(Panel {
instance: self.instance.clone(),
abi: self.abi.clone(),
id,
userdata,
})
@ -54,6 +63,7 @@ impl Script {
/// A Canary panel.
pub struct Panel {
instance: Arc<dyn Instance>,
abi: Arc<ScriptAbi>,
id: PanelId,
userdata: u32,
}
@ -78,33 +88,15 @@ impl Panel {
pub fn on_message(&self, msg: Vec<u8>) {
self.instance.on_message(self.userdata, msg);
}
pub fn recv_messages(&self) -> Vec<Vec<u8>> {
self.abi.recv_panel_messages(self.id)
}
}
/// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement).
pub const PX_PER_MM: f32 = 25.4 / 96.0;
/// Low-level script API callbacks.
///
/// If you're a casual user of Canary the struct you're looking for is
/// [ScriptAbiImpl]. This trait exists to help with making mocks for testing.
pub trait ScriptAbi {
fn start_draw(&self);
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]);
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color);
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand]));
fn font_load(&self, family: &str) -> u32;
fn text_layout_new(&self, font_id: u32, text: &str) -> u32;
fn text_layout_delete(&self, id: u32);
fn text_layout_get_bounds(&self, id: u32, rect: &mut Rect);
fn message_new(&self, data: Vec<u8>) -> u32;
fn message_free(&self, id: u32);
fn message_get_len(&self, id: u32) -> u32;
fn message_get_data(&self, id: u32, dst: &mut [u8]);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PanelId(pub(crate) usize);
@ -116,95 +108,3 @@ pub enum DrawCommand {
indices: Vec<MeshIndex>,
},
}
/// The standard [ScriptAbi] implementation to use.
#[derive(Default)]
pub struct ScriptAbiImpl {
draw_cmds: Mutex<Vec<DrawCommand>>,
font_store: text::FontStore,
font_families: Mutex<HashMap<String, u32>>,
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
text_layouts: RwLock<Slab<text::TextLayout>>,
message_store: RwLock<Slab<Vec<u8>>>,
}
impl ScriptAbi for ScriptAbiImpl {
fn start_draw(&self) {
let mut lock = self.draw_cmds.lock();
lock.clear();
}
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
self.draw_cmds.lock().push(DrawCommand::Mesh {
vertices: vertices.to_vec(),
indices: indices.to_vec(),
})
}
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
// TODO multiple fonts per layout
let layouts = self.text_layouts.read();
let layout = layouts.get(id as usize).unwrap();
let glyphs = layout.glyphs.as_slice();
let loaded = self.loaded_fonts.read();
let font = loaded.get(layout.font_id as usize).unwrap();
let cmds = font.draw(glyphs, offset, scale, color);
self.draw_cmds.lock().extend(cmds.into_iter());
}
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
f(self.draw_cmds.lock().as_slice());
}
fn font_load(&self, family: &str) -> u32 {
let mut family_cache = self.font_families.lock();
if let Some(cached) = family_cache.get(family) {
return *cached;
}
let font = self.font_store.load_font(family);
let mut loaded = self.loaded_fonts.write();
let id = loaded.len() as u32;
family_cache.insert(family.to_string(), id);
loaded.push(font);
id
}
fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
let loaded = self.loaded_fonts.read();
let font = loaded.get(font_id as usize).unwrap();
let layout = font.shape(text);
self.text_layouts.write().insert(layout) as u32
}
fn text_layout_delete(&self, id: u32) {
self.text_layouts.write().remove(id as usize);
}
fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
let _ = std::mem::replace(dst, src);
}
fn message_new(&self, data: Vec<u8>) -> u32 {
let mut store = self.message_store.write();
let id = store.insert(data) as u32;
id
}
fn message_free(&self, id: u32) {
let mut store = self.message_store.write();
store.remove(id as usize);
}
fn message_get_len(&self, id: u32) -> u32 {
self.message_store.read().get(id as usize).unwrap().len() as u32
}
fn message_get_data(&self, id: u32, dst: &mut [u8]) {
let store = self.message_store.read();
let src = store.get(id as usize).unwrap();
dst.copy_from_slice(src);
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later
use super::{AllsortsFont, Rect, Vec2};
@ -133,26 +133,8 @@ impl OutlineSink {
fn pf_vector_to_lyon(&mut self, v: &Vector2F) -> lyon::geom::Point<f32> {
let point = lyon::geom::Point::<f32>::new(v.x(), -v.y()) / self.units_per_em;
// TODO clean this up with helper math methods?
let bb = &mut self.bounding_box;
if point.x < bb.bl.x {
bb.bl.x = point.x;
}
if point.x > bb.tr.x {
bb.tr.x = point.x;
}
if point.y < bb.tr.y {
bb.tr.y = point.y;
}
if point.y > bb.bl.y {
bb.bl.y = point.y;
}
let glam_point = Vec2::new(point.x, point.y);
self.bounding_box = self.bounding_box.union_point(glam_point);
point
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
// SPDX-License-Identifier: LGPL-3.0-or-later
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
@ -73,31 +73,9 @@ impl Font {
xcur += position.hori_advance;
ycur += position.vert_advance;
let xpos = xpos as f32 / units_per_em;
let ypos = ypos as f32 / units_per_em;
let mut bb = glyphs.get(position.index as usize).unwrap().bounding_box;
bb.bl.x = bb.bl.x + xpos;
bb.bl.y = bb.bl.y + ypos;
bb.tr.x = bb.tr.x + xpos;
bb.tr.y = bb.tr.y + ypos;
// TODO use euclid instead
if bounds.bl.x > bb.bl.x {
bounds.bl.x = bb.bl.x;
}
if bounds.bl.y > bb.bl.y {
bounds.bl.y = bb.bl.y;
}
if bounds.tr.x < bb.tr.x {
bounds.tr.x = bb.tr.x;
}
if bounds.tr.y < bb.tr.y {
bounds.tr.y = bb.tr.y;
}
let pos = Vec2::new(xpos as f32, ypos as f32) / units_per_em;
let bb = glyphs.get(position.index as usize).unwrap().bounding_box;
bounds = bounds.union(&bb.offset(pos));
}
TextLayout {
@ -242,6 +220,7 @@ impl Default for FontStore {
impl FontStore {
pub fn new() -> Self {
log::info!("Initializing FontStore");
let source = font_kit::source::SystemSource::new();
let source = Box::new(source);
@ -263,14 +242,14 @@ impl FontStore {
use font_kit::handle::Handle;
use font_kit::properties::Properties;
println!("loading font family {}", title);
log::info!("Finding font by family: {}", title);
let font_handle = self
.source
.select_best_match(&[FamilyName::Title(title.to_string())], &Properties::new())
.unwrap();
println!("loading font file: {:?}", font_handle);
log::info!("Loading font file: {:?}", font_handle);
let font_data = if let Handle::Path {
path,