197 Commits
Author SHA1 Message Date
selimaj-dev 0cead353ee macros fix 2026-01-31 21:15:20 +01:00
selimaj-dev f5cd3e7fbf macros fix 2026-01-31 21:14:26 +01:00
selimaj-dev e0171a9029 v0.2.0 2026-01-31 21:12:15 +01:00
selimaj-dev 73cfc36277 Documented 2026-01-31 18:21:46 +01:00
Klesti Selimaj 0f5422b66b Merge pull request #46 from osui-rs/scope-performance-patch
Scope performance patch
2026-01-31 17:55:21 +01:00
selimaj-dev dc2810b6d8 Readme update #40 2026-01-31 17:48:51 +01:00
selimaj-dev 8ebef06808 Benchmark 2026-01-31 17:47:08 +01:00
selimaj-dev 04f2abc47a Made scope back to Mutex due to performance 2026-01-31 17:45:14 +01:00
selimaj-dev 1f566e19e7 Improved draw_children performance 2026-01-31 16:16:08 +01:00
selimaj-dev 457b894864 Benchmark 2026-01-31 15:48:21 +01:00
selimaj-dev 89161ab39b Fixed draw_children 2026-01-31 15:46:59 +01:00
Klesti Selimaj 5017895a30 Merge pull request #45 from osui-rs/access-cell
Access cell
2026-01-31 15:04:56 +01:00
selimaj-dev dd7c53c9c1 Benchmarked 2026-01-31 15:00:16 +01:00
selimaj-dev 4e2636360f Fixed rsx emit with new AccessCell 2026-01-31 14:56:41 +01:00
selimaj-dev 2faac93944 Abstracted children in scope 2026-01-31 14:52:05 +01:00
selimaj-dev d8173b8ab3 Scope children with AccessCell 2026-01-31 14:44:28 +01:00
selimaj-dev 8685cdde2a Scopes with AccessCell 2026-01-31 14:32:49 +01:00
selimaj-dev ec320e47fd Access cell in event handlers 2026-01-31 14:19:37 +01:00
selimaj-dev 17f78b86f4 Implemented access cell for component and view 2026-01-31 13:02:22 +01:00
Klesti Selimaj d8f24d7eec Merge pull request #44 from osui-rs/benchmark
Benchmark
2026-01-31 01:48:00 +01:00
selimaj-dev d9e2006117 Benchmark 2026-01-31 01:44:50 +01:00
Klesti Selimaj 78c3e7f7b3 Merge pull request #43 from osui-rs/developed-structure
Developed structure
2026-01-31 00:30:43 +01:00
selimaj-dev e5812315c6 Hide cursor in engine and crossterm in prelude 2026-01-31 00:23:10 +01:00
selimaj-dev 85342d641f Improved prelude 2026-01-31 00:15:40 +01:00
selimaj-dev ab9548920c Improved rsx parsing 2026-01-31 00:09:14 +01:00
selimaj-dev eefa41318c Refactored component 2026-01-30 23:36:54 +01:00
selimaj-dev 4e2d4662d1 Display to rsx 2026-01-30 23:25:33 +01:00
selimaj-dev bf6fa42d96 Fixed simple example 2026-01-30 23:14:10 +01:00
selimaj-dev e31c17ba41 Rsx children scope placing 2026-01-30 23:13:05 +01:00
selimaj-dev 74e33fa4c2 Frontend generate children 2026-01-30 23:00:02 +01:00
selimaj-dev 03191b0a57 Rsx children 2026-01-30 22:54:56 +01:00
selimaj-dev 91f0878edc rsx params 2026-01-30 21:14:41 +01:00
selimaj-dev e85257a10e component generator 2026-01-30 19:29:52 +01:00
selimaj-dev 1f64b4f6e2 Component params 2026-01-30 19:02:26 +01:00
selimaj-dev 2799faaa73 Small stuff 2026-01-30 18:23:46 +01:00
selimaj-dev 1526daaba2 Chunked threads 2026-01-30 18:07:28 +01:00
selimaj-dev 5e2e9952d9 Benchmark result in microseconds 2026-01-30 17:39:15 +01:00
selimaj-dev 340b8c6f4d Multithreaded benchmark 2026-01-30 17:35:58 +01:00
selimaj-dev c9a7e3c835 Improved benchmark 2026-01-30 17:15:12 +01:00
selimaj-dev 41f4ad1022 Fixed rsx! for loops and if statements 2026-01-30 17:03:18 +01:00
selimaj-dev 41e7b2a10c Benchmarking 2026-01-30 15:08:01 +01:00
selimaj-dev 2d79b1dfbb Implemented engine return 2026-01-30 14:19:24 +01:00
selimaj-dev ccbbf29538 Separated simple benchmark 2026-01-29 10:57:17 +01:00
selimaj-dev 74c4a976dd Separated simple benchmark 2026-01-29 10:38:07 +01:00
selimaj-dev 059f8f07c1 Nanosecond benchmarking 2026-01-29 09:59:06 +01:00
Klesti Selimaj 75a7f20e4a Merge pull request #42 from osui-rs/engine-commands
Engine commands
2026-01-28 23:53:06 +01:00
selimaj-dev 0cf0eb42fc Context implementation 2026-01-28 23:50:08 +01:00
selimaj-dev 93ba2f4a78 Implemented exectution to Context 2026-01-28 23:41:54 +01:00
selimaj-dev 1df6cd4c5e Implemented commandExecutor logic 2026-01-28 23:33:22 +01:00
selimaj-dev 63abf41094 Implemented commands on scopes 2026-01-28 19:33:12 +01:00
selimaj-dev 715175c412 rsx proc macro 2026-01-28 15:27:38 +01:00
selimaj-dev 46587ff822 rsx proc macro 2026-01-28 15:21:45 +01:00
selimaj-dev 011b1ab963 Engine structure 2026-01-28 10:27:18 +01:00
Klesti Selimaj 665b502a09 Merge pull request #39 from osui-rs/frontend
Frontend
2026-01-26 17:54:34 +01:00
selimaj-dev 506ccae049 State syncing 2026-01-26 17:27:47 +01:00
selimaj-dev 86e3f4e684 Sync state (param passing) 2026-01-26 16:58:44 +01:00
selimaj-dev 89b460697f Any event 2026-01-26 09:38:36 +01:00
selimaj-dev bd53e6450c use effect instead of event 2026-01-26 09:34:23 +01:00
selimaj-dev 083812458b Children clear for loop 2026-01-25 21:12:34 +01:00
selimaj-dev 4364177538 Text @position 2026-01-25 21:11:42 +01:00
selimaj-dev 98598bec15 Initial dynamic render 2026-01-25 20:46:26 +01:00
selimaj-dev eb702dce45 RSX dependency 2026-01-25 20:40:24 +01:00
selimaj-dev 229353f5f0 RSX dependency 2026-01-25 20:39:17 +01:00
selimaj-dev 04a69b138a RSX For loop 2026-01-25 20:12:47 +01:00
selimaj-dev 993a790da7 If statement rsx 2026-01-25 20:00:21 +01:00
selimaj-dev ecc1172737 Rsx scope child 2026-01-25 19:22:36 +01:00
selimaj-dev a9ecc8da8b working Rsx 2026-01-25 18:58:24 +01:00
selimaj-dev f6a4bbd8ac Frontend structure 2026-01-25 18:38:07 +01:00
selimaj-dev 4afe1c24c2 Added use_mount 2026-01-25 17:22:40 +01:00
Klesti Selimaj b50ca3263b Merge pull request #38 from osui-rs/render-scoping
Render scoping
2026-01-25 17:15:04 +01:00
selimaj-dev 6b3e9239c9 Scope view 2026-01-25 02:27:36 +01:00
selimaj-dev 470512a542 Dynamic scoping with for loop 2026-01-25 02:22:16 +01:00
selimaj-dev 6e481862ec Dynamic scoping with if statement 2026-01-25 02:17:08 +01:00
selimaj-dev 05a390b10f Dynamic scoping 2026-01-25 00:30:53 +01:00
selimaj-dev 926405a863 Dynamic scoping 2026-01-24 21:38:45 +01:00
selimaj-dev 9953e4a0bc Render position 2026-01-24 17:46:33 +01:00
selimaj-dev 8f0712d4f8 Event passing 2026-01-24 17:31:58 +01:00
selimaj-dev 9d55dacde2 Context children 2026-01-24 17:22:59 +01:00
selimaj-dev abe37686d8 Refresh on children 2026-01-24 16:40:03 +01:00
selimaj-dev 02f3aea4e3 Fixed refresh 2026-01-24 16:01:22 +01:00
selimaj-dev 33370287b2 Renderer 2026-01-24 14:49:47 +01:00
selimaj-dev 0f12a371e5 Renderer 2026-01-24 14:47:53 +01:00
selimaj-dev 1f9bf48bb4 View render 2026-01-24 14:37:19 +01:00
selimaj-dev 49bb99fedb Simple test 2026-01-24 14:26:04 +01:00
selimaj-dev 979af999e4 Simple renderer 2026-01-24 14:17:05 +01:00
selimaj-dev 0515087f0d Renderer 2026-01-24 10:44:30 +01:00
selimaj-dev 2a62e53ed5 Engine 2026-01-24 01:00:21 +01:00
selimaj-dev c2b9bb3aa1 DrawContext size 2026-01-24 00:42:54 +01:00
selimaj-dev 0a026c4ee7 View 2026-01-24 00:28:43 +01:00
selimaj-dev 15ff801cb0 Emit event threaded 2025-11-23 18:01:21 +01:00
selimaj-dev 538cc8731f Improved state mangement 2025-11-23 11:23:28 +01:00
selimaj-dev b326fc48ed Improved state management 2025-11-23 11:10:07 +01:00
selimaj-dev 8740bbee89 Working event handling 2025-11-23 10:37:20 +01:00
selimaj-dev 22d02e7d1a Multithreaded 2025-11-23 09:58:01 +01:00
selimaj-dev 0cefbaa223 Testing 2025-11-20 23:23:01 +01:00
selimaj-dev b57fffe9b7 Testing 2025-11-20 23:15:31 +01:00
selimaj-dev 4d2f0d7b9d State management 2025-11-20 17:17:18 +01:00
selimaj-dev ff351caee0 Testing new structure 2025-11-20 17:10:12 +01:00
selimaj-dev e4110976cd Testing new structure 2025-11-20 16:57:14 +01:00
selimaj-dev 1fbff7a0bc Testing new structure 2025-11-19 09:14:03 +01:00
selimaj-dev 6e8ca5535e Testing new structure 2025-11-18 20:17:02 +01:00
selimaj-dev e031f9bcef Testing new structure 2025-11-18 20:04:14 +01:00
Klesti Selimaj c6d11315d6 Merge pull request #36 from osui-rs/refactor
Refactor
2025-11-02 12:31:59 +01:00
selimaj-dev 68d00be96c Simple velocity example 2025-11-02 10:49:25 +01:00
selimaj-dev f5d1b42db4 Velocity element 2025-11-02 10:26:15 +01:00
selimaj-dev 63ed7b8da4 Y offset fix on print 2025-11-02 09:16:03 +01:00
selimaj-dev 98b2a4e667 Outline position fix 2025-11-02 09:10:34 +01:00
selimaj-dev e4b905d085 Refactored render scope 2025-11-02 09:00:33 +01:00
Klesti Selimaj 1d6ce1506e Merge pull request #34 from osui-rs/scroll
Scroll
2025-11-01 23:34:04 +01:00
selimaj-dev 0e3c1ccafe Fixed bad scissor 2025-11-01 23:19:22 +01:00
selimaj-dev dbd16956dd Seperated row and column flex 2025-11-01 22:50:19 +01:00
selimaj-dev c91a473263 Added auth example 2025-11-01 22:47:27 +01:00
selimaj-dev 6424ad3390 Small change in iter 2025-11-01 16:58:10 +01:00
selimaj-dev 23edb6c917 Mouse input 2025-11-01 15:30:03 +01:00
selimaj-dev 72e8df007e Scroll 2025-11-01 15:14:54 +01:00
selimaj-dev a2350f10f5 Dynamic flex size 2025-11-01 13:30:41 +01:00
selimaj-dev 2d41f3b50b Fixed padding 2025-11-01 13:02:10 +01:00
selimaj-dev 5cb6d341af Even width and height comparisons 2025-11-01 13:01:44 +01:00
selimaj-dev f41a4936f9 Fixed paginator render 2025-11-01 12:45:46 +01:00
selimaj-dev dbb8a0e01a Fixed area rendering and merging issues 2025-11-01 12:40:30 +01:00
selimaj-dev fd8e466830 Parent transform 2025-11-01 11:56:55 +01:00
selimaj-dev 29432a939b Paginator 2025-11-01 09:48:04 +01:00
selimaj-dev 9d4b247b2a Working area stop 2025-11-01 09:25:05 +01:00
selimaj-dev 916818f915 1k commits 🥳 (Fixed delete key not working in input) 2025-09-05 20:26:37 +02:00
Leo Codes 5cf59f751d Merge pull request #32 from osui-rs/use-effect
Added a `run!` macro for useEffect behavior
2025-08-14 09:43:30 -04:00
selimaj-dev c460e073a7 Updated the run macro #31 #30 2025-08-14 15:42:21 +02:00
selimaj-dev f51182b2b1 Updated the run macro #30 #31 2025-08-14 15:40:39 +02:00
selimaj-dev 6100023a2a Added a run macro with useEffect like properties #30 2025-08-14 05:01:03 +02:00
Leo Codes 3d9aa35964 Merge pull request #29 from osui-rs/iteration-patch
Added iteration and improved examples
2025-08-12 23:43:39 -04:00
selimaj-dev 9a03f241d8 Added rsx iteration 2025-08-12 23:40:56 -04:00
selimaj-dev fde8ad17d6 added Iterator 2025-08-12 23:18:35 -04:00
selimaj-dev e0cf149790 Updated examples with a few small changes to resolve warnings 2025-08-12 21:12:54 -04:00
Leo Codes 1e590597f3 Merge pull request #28 from osui-rs/render-refactor
Render refactor
2025-08-10 14:46:47 -04:00
selimaj-dev 935f18178e changed demo exit key from q to escape 2025-08-10 13:45:45 -05:00
selimaj-dev 20a9d2b216 Improved ghost with paginator 2025-08-09 02:44:17 +02:00
selimaj-dev 3520369e35 Added ghost elements to avoid focusing on them 2025-08-09 02:27:16 +02:00
selimaj-dev c8ebe36d78 Added focus cursor in input 2025-08-08 23:51:15 +02:00
selimaj-dev d3861909e7 added RenderContext 2025-08-08 23:47:21 +02:00
selimaj-dev 1bdbc447ab Merge branch 'render-refactor' of github.com:osui-rs/osui into render-refactor 2025-08-08 23:38:21 +02:00
selimaj-dev e0059585ce Implemented the ElementRenderer for widgets 2025-08-04 12:35:56 -05:00
selimaj-dev b363c81a24 Added a ElementRenderer trait for element renderers 2025-08-04 12:16:23 -05:00
Leo Codes 2fae5c71f8 Merge pull request #27 from osui-rs/elem-nav
Elem nav
2025-08-04 08:25:54 -05:00
selimaj-dev 9868f02c33 Version update to 0.1.1 (for later) 2025-08-04 08:24:35 -05:00
selimaj-dev 896dfb0a36 Added a AlwaysFocused component #26 2025-08-04 08:23:51 -05:00
selimaj-dev 3a5286a54f Added focus #26 2025-08-04 08:16:34 -05:00
selimaj-dev ca4ee54e9d Added after render and render on extension with child handlers #26 2025-08-04 05:58:11 -05:00
selimaj-dev 62e0c1eac1 Added event on Extension #26 2025-08-04 04:50:52 -05:00
selimaj-dev 48397c06fa Added ExtensionContext #26 2025-08-04 04:45:55 -05:00
selimaj-dev 14602f95c8 First widget focus #26 2025-08-04 04:24:41 -05:00
selimaj-dev fbd84f98d3 Added focus on widget #26 2025-08-04 04:17:40 -05:00
selimaj-dev db1de03fbb Refactored widget #26 2025-08-04 04:05:38 -05:00
Leo Codes 6569a9eb20 Merge pull request #25 from osui-rs/more-elements
More elements
2025-08-03 13:25:54 -05:00
selimaj-dev 2c7d3cc80f added .sparkignore 2025-08-03 13:24:21 -05:00
Leo Codes 2383a7b42a Merge pull request #24 from osui-rs/more-elements
More elements
2025-08-03 09:33:06 -05:00
selimaj-dev d59c3d7911 Added a NoRenderRoot component for root only and NoRender doesn't render on child handlers 2025-08-03 09:20:25 -05:00
selimaj-dev 1aa78bc1a4 Fixed input still typing with modifiers like ctrl 2025-08-03 09:11:50 -05:00
selimaj-dev f8f924ce25 Fixed input still typing with modifiers like ctrl 2025-08-03 09:11:04 -05:00
selimaj-dev ea9afb0e64 Added a paginator #19 2025-08-03 09:04:48 -05:00
selimaj-dev 71299046d3 Mutable draw_child so no more mutex needed 2025-08-03 09:04:24 -05:00
selimaj-dev acce51c372 added a input element #19 2025-08-02 17:15:55 -05:00
selimaj-dev ba04a82980 Updated RenderScope with positions 2025-08-02 17:07:09 -05:00
selimaj-dev ec3576e6db Element events #19 2025-08-02 16:26:38 -05:00
Leo Codes e0ae338e15 Merge pull request #23 from osui-rs/foreground-style
Functional foreground #22
2025-08-02 13:15:42 -05:00
selimaj-dev d2757ad7b0 Functional foreground #22 2025-08-02 13:14:51 -05:00
Leo Codes de039fac96 Merge pull request #21 from osui-rs/rsx-patch-args
Rsx patch args
2025-08-02 12:59:23 -05:00
selimaj-dev 2d8755b13c added heading #19 2025-08-02 12:58:45 -05:00
selimaj-dev d9d107bd80 Added rsx element fields #20 2025-08-02 12:19:04 -05:00
selimaj-dev 35dcc4d22c v0.1.0 2025-08-02 11:21:40 -05:00
selimaj-dev 34b5799644 Added a close function #18 2025-08-02 06:58:13 -05:00
selimaj-dev 911968a1cb input handling #17 2025-08-02 06:47:03 -05:00
Leo Codes d00fbb6565 Merge pull request #16 from osui-rs/demos-module
Demos module
2025-08-02 06:17:27 -05:00
selimaj-dev 2892176a76 Square outline demo fix #15 2025-08-02 06:16:09 -05:00
selimaj-dev 36e06732d9 Padding fix for flex 2025-08-02 06:15:24 -05:00
selimaj-dev 3a4c3505ac Padding fix for flex 2025-08-02 06:14:13 -05:00
selimaj-dev 0ccf03de3b Simple demos #15 2025-08-02 06:02:54 -05:00
selimaj-dev 48849c7cc2 use_area instead of draw_rect for div 2025-08-02 05:55:35 -05:00
selimaj-dev 2b0d85549f Simple demo #15 2025-08-02 05:48:57 -05:00
Leo Codes 598ab5b54d Merge pull request #14 from osui-rs/code-cleanup
Code cleanup
2025-08-01 20:36:26 -05:00
selimaj-dev 095aa14b82 document widget.rs (some unfinished) #13 2025-08-01 20:34:32 -05:00
selimaj-dev d1008a69c9 document style.rs #13 2025-08-01 20:29:41 -05:00
selimaj-dev a30afdeb28 document state.rs #13 2025-08-01 20:26:21 -05:00
selimaj-dev 1debb6a576 document render_scope.rs #13 2025-08-01 20:24:43 -05:00
selimaj-dev fc0406e412 document macros.rs #13 2025-08-01 20:23:03 -05:00
selimaj-dev 27580e773a document frontend.rs #13 2025-08-01 20:20:20 -05:00
selimaj-dev a1499064ac document lib #13 2025-08-01 20:16:18 -05:00
selimaj-dev c2563358b6 document lib #13 2025-08-01 20:02:55 -05:00
selimaj-dev b45ef950a1 changed % state dependency to be after the components 2025-08-01 19:47:41 -05:00
selimaj-dev 063c90d986 #13 cleanup frontend and widgets 2025-08-01 19:44:55 -05:00
selimaj-dev da1ecb1f21 #13 cleanup extensions 2025-08-01 19:25:14 -05:00
selimaj-dev 75827260cc cleanup elements #13 2025-08-01 19:24:39 -05:00
Leo Codes 9a91fb3af0 Merge pull request #12 from osui-rs/better-state-management
Better state management
2025-08-01 16:34:26 -05:00
selimaj-dev bbff192788 Updated readme for the new syntax with rsx 2025-08-01 16:33:35 -05:00
selimaj-dev 73e2c214ee Refactored the frontend to work with static widgets #11 2025-08-01 16:32:56 -05:00
selimaj-dev 45db1fb774 Refactored the widget system #11 2025-08-01 16:10:56 -05:00
selimaj-dev af5428a2f3 area draw 2025-08-01 14:46:59 -05:00
selimaj-dev b227f19a9c padding 2025-07-30 01:14:38 +02:00
selimaj-dev ae16c9a69e background 2025-07-30 00:15:40 +02:00
38 changed files with 2161 additions and 1506 deletions
+5
View File
@@ -0,0 +1,5 @@
debug
target
github
Cargo.lock
.DS_Store
+8 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "osui" name = "osui"
version = "0.0.9-alpha.2" version = "0.2.0"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
description = "A TUI library for advanced uis" description = "A TUI library for advanced uis"
@@ -9,8 +9,7 @@ homepage = "https://osui.netlify.app"
documentation = "https://osui.netlify.app/docs/" documentation = "https://osui.netlify.app/docs/"
[features] [features]
no_rsx = [] rsx = []
no_elem = []
[lib] [lib]
name = "osui" name = "osui"
@@ -24,7 +23,10 @@ path = "src/main.rs"
cc = "1.0" cc = "1.0"
[dependencies] [dependencies]
access-cell = "0.1.3"
crossterm = "0.28.1" crossterm = "0.28.1"
figlet-rs = "0.1.5"
osui-macros = { version = "0.1.0", path = "macros" }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
@@ -42,3 +44,6 @@ overflow-checks = false
lto = true lto = true
panic = 'abort' panic = 'abort'
codegen-units = 1 codegen-units = 1
[workspace]
members = [".", "macros"]
+12 -9
View File
@@ -17,20 +17,23 @@
</p> </p>
<p align="center"> <p align="center">
<b>OSUI is a customizable terminal user interface (TUI) library written in Rust. It provides a set of components and rsx to build interactive command-line interfaces with ease.</b> <b>OSUI is a customizable terminal user interface (TUI) library written in Rust.</b>
</p> </p>
```rust ```rust
use osui::{style::Transform, Screen}; use osui::prelude::*;
fn main() -> std::io::Result<()> { pub fn main() {
let screen = Screen::new(); let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
screen #[component]
.draw(format!("Hello, World")) fn App(cx: &Arc<Context>) -> View {
.component(Transform::center()); rsx! {
"Hello World"
screen.run() }
.view(&cx)
} }
``` ```
+227
View File
@@ -0,0 +1,227 @@
Iterx72,Nestingx72,Time µs
14,14,11964
0,1,1078
5,8,1818
10,13,2632
14,2,3392
0,9,520
9,3,2360
10,5,2551
10,11,2548
6,8,1738
12,12,2922
0,8,601
12,4,2919
0,11,521
6,4,1942
11,7,2741
9,4,2496
5,9,1764
13,9,3069
2,5,1053
5,11,1790
14,1,3581
5,0,962
0,3,845
1,3,868
7,8,2013
12,3,2987
9,8,2328
10,7,2533
11,1,2766
7,1,1938
0,5,687
6,9,1830
10,14,2628
6,12,1786
4,0,835
4,14,2464
10,1,2633
12,7,2948
2,9,1066
4,6,2646
7,11,1999
5,13,1748
13,4,3110
13,13,3094
3,5,1166
11,12,2745
1,2,814
9,5,2419
2,11,1117
1,13,835
11,11,2759
1,12,882
9,1,2457
0,6,680
5,12,1794
9,11,2326
10,9,2546
10,10,2604
13,2,3250
13,7,3113
7,10,1959
1,14,833
6,6,1886
14,7,3339
6,7,1824
11,6,2756
12,6,3007
14,6,3315
4,3,11464
2,14,924
7,12,1966
11,14,2817
9,10,2367
1,11,827
8,14,2184
14,0,643
11,3,2781
2,4,1123
12,8,2950
13,6,3160
3,13,1289
7,7,1989
3,9,1142
3,6,1168
4,8,2788
14,12,3346
1,1,805
5,10,1781
7,13,1988
5,5,2015
0,14,520
11,8,2890
3,10,1140
13,10,3235
3,14,1185
8,10,2183
12,10,2896
2,8,1072
9,6,2358
13,1,3066
3,8,1133
11,10,2758
4,13,2550
1,0,643
4,2,5482
3,7,1155
1,4,726
3,3,1192
4,11,2576
8,0,619
9,2,2444
10,3,2581
4,5,5400
5,3,2444
1,10,804
10,2,2563
0,7,592
0,2,802
1,5,741
2,7,984
10,0,755
13,14,3112
5,14,1811
8,12,2180
6,11,1723
4,7,2897
10,8,2536
1,7,813
6,0,699
8,2,2187
5,7,1811
14,8,3304
2,0,661
8,13,2185
1,9,786
3,0,623
12,5,3012
8,1,2203
8,11,2285
14,10,3413
4,10,2613
11,5,2729
6,1,2035
12,1,2891
5,4,2036
2,3,1083
14,3,3312
3,2,1278
5,2,2392
8,7,2151
5,6,2072
3,12,1230
2,10,954
2,13,932
13,8,3128
0,12,521
11,4,2769
7,2,1947
4,1,1802
8,8,2163
7,3,1966
13,3,3207
1,8,746
12,9,2955
10,12,2570
9,9,2388
4,12,2566
7,0,619
7,9,2004
13,0,607
14,9,3355
3,4,1154
11,2,2753
11,13,2739
6,13,1735
7,6,1983
8,3,2166
7,14,1958
6,2,2101
13,5,3221
8,4,2120
10,4,2716
14,5,3264
14,13,3299
11,9,2725
13,11,3132
1,6,809
2,2,1155
4,4,2242
8,6,2218
12,0,645
7,4,1925
2,6,1054
9,13,2340
13,12,3112
14,4,3334
3,11,1139
12,14,3004
4,9,2649
0,13,527
12,11,2976
0,0,1559
12,13,2952
6,10,1749
14,11,3365
11,0,647
2,12,1024
10,6,2562
8,5,2227
9,12,2363
2,1,1023
0,10,528
6,3,2005
8,9,2195
7,5,1966
6,14,1755
9,0,608
14,14,3389
9,7,2391
0,4,688
5,1,2574
9,14,2353
12,2,2975
3,1,1276
6,5,1804
1 Iterx72 Nestingx72 Time µs
2 14 14 11964
3 0 1 1078
4 5 8 1818
5 10 13 2632
6 14 2 3392
7 0 9 520
8 9 3 2360
9 10 5 2551
10 10 11 2548
11 6 8 1738
12 12 12 2922
13 0 8 601
14 12 4 2919
15 0 11 521
16 6 4 1942
17 11 7 2741
18 9 4 2496
19 5 9 1764
20 13 9 3069
21 2 5 1053
22 5 11 1790
23 14 1 3581
24 5 0 962
25 0 3 845
26 1 3 868
27 7 8 2013
28 12 3 2987
29 9 8 2328
30 10 7 2533
31 11 1 2766
32 7 1 1938
33 0 5 687
34 6 9 1830
35 10 14 2628
36 6 12 1786
37 4 0 835
38 4 14 2464
39 10 1 2633
40 12 7 2948
41 2 9 1066
42 4 6 2646
43 7 11 1999
44 5 13 1748
45 13 4 3110
46 13 13 3094
47 3 5 1166
48 11 12 2745
49 1 2 814
50 9 5 2419
51 2 11 1117
52 1 13 835
53 11 11 2759
54 1 12 882
55 9 1 2457
56 0 6 680
57 5 12 1794
58 9 11 2326
59 10 9 2546
60 10 10 2604
61 13 2 3250
62 13 7 3113
63 7 10 1959
64 1 14 833
65 6 6 1886
66 14 7 3339
67 6 7 1824
68 11 6 2756
69 12 6 3007
70 14 6 3315
71 4 3 11464
72 2 14 924
73 7 12 1966
74 11 14 2817
75 9 10 2367
76 1 11 827
77 8 14 2184
78 14 0 643
79 11 3 2781
80 2 4 1123
81 12 8 2950
82 13 6 3160
83 3 13 1289
84 7 7 1989
85 3 9 1142
86 3 6 1168
87 4 8 2788
88 14 12 3346
89 1 1 805
90 5 10 1781
91 7 13 1988
92 5 5 2015
93 0 14 520
94 11 8 2890
95 3 10 1140
96 13 10 3235
97 3 14 1185
98 8 10 2183
99 12 10 2896
100 2 8 1072
101 9 6 2358
102 13 1 3066
103 3 8 1133
104 11 10 2758
105 4 13 2550
106 1 0 643
107 4 2 5482
108 3 7 1155
109 1 4 726
110 3 3 1192
111 4 11 2576
112 8 0 619
113 9 2 2444
114 10 3 2581
115 4 5 5400
116 5 3 2444
117 1 10 804
118 10 2 2563
119 0 7 592
120 0 2 802
121 1 5 741
122 2 7 984
123 10 0 755
124 13 14 3112
125 5 14 1811
126 8 12 2180
127 6 11 1723
128 4 7 2897
129 10 8 2536
130 1 7 813
131 6 0 699
132 8 2 2187
133 5 7 1811
134 14 8 3304
135 2 0 661
136 8 13 2185
137 1 9 786
138 3 0 623
139 12 5 3012
140 8 1 2203
141 8 11 2285
142 14 10 3413
143 4 10 2613
144 11 5 2729
145 6 1 2035
146 12 1 2891
147 5 4 2036
148 2 3 1083
149 14 3 3312
150 3 2 1278
151 5 2 2392
152 8 7 2151
153 5 6 2072
154 3 12 1230
155 2 10 954
156 2 13 932
157 13 8 3128
158 0 12 521
159 11 4 2769
160 7 2 1947
161 4 1 1802
162 8 8 2163
163 7 3 1966
164 13 3 3207
165 1 8 746
166 12 9 2955
167 10 12 2570
168 9 9 2388
169 4 12 2566
170 7 0 619
171 7 9 2004
172 13 0 607
173 14 9 3355
174 3 4 1154
175 11 2 2753
176 11 13 2739
177 6 13 1735
178 7 6 1983
179 8 3 2166
180 7 14 1958
181 6 2 2101
182 13 5 3221
183 8 4 2120
184 10 4 2716
185 14 5 3264
186 14 13 3299
187 11 9 2725
188 13 11 3132
189 1 6 809
190 2 2 1155
191 4 4 2242
192 8 6 2218
193 12 0 645
194 7 4 1925
195 2 6 1054
196 9 13 2340
197 13 12 3112
198 14 4 3334
199 3 11 1139
200 12 14 3004
201 4 9 2649
202 0 13 527
203 12 11 2976
204 0 0 1559
205 12 13 2952
206 6 10 1749
207 14 11 3365
208 11 0 647
209 2 12 1024
210 10 6 2562
211 8 5 2227
212 9 12 2363
213 2 1 1023
214 0 10 528
215 6 3 2005
216 8 9 2195
217 7 5 1966
218 6 14 1755
219 9 0 608
220 14 14 3389
221 9 7 2391
222 0 4 688
223 5 1 2574
224 9 14 2353
225 12 2 2975
226 3 1 1276
227 6 5 1804
+65
View File
@@ -0,0 +1,65 @@
use osui::prelude::*;
use std::collections::HashMap;
pub fn main() {
let engine = Arc::new(Benchmark::new(Console::new()));
let mut benchmark_result: HashMap<(usize, usize), BenchmarkResult> = HashMap::new();
for i in 0..15 {
for n in 0..15 {
let res = {
let mut results = Vec::with_capacity(6);
for _ in 0..6 {
results.push(
engine
.run(App {
n: n * 72,
i: i * 72,
})
.expect("Failed to run engine"),
);
}
results.sort_by_key(|r| r.total_render);
results[3].clone()
};
benchmark_result.insert((i, n as usize), res);
}
}
let max = benchmark_result
.values()
.map(|b| b.total_render)
.max()
.unwrap_or(0);
println!("Iterx72,Nestingx72,Time µs\n14,14,{}", { max + 500 });
for ((i, n), bench) in benchmark_result.iter() {
println!("{i},{n},{}", bench.total_render);
}
}
#[component]
fn App(cx: &Arc<Context>, n: usize, i: usize) -> View {
let n = n.clone();
let i = i.clone();
if n == 0 {
rsx! {
"Hello, world!"
}
.view(&cx)
} else {
rsx! {
for _ in (0..i) {
App { n: n - 1, i: 0 }
}
}
.view(&cx)
}
}
+14
View File
@@ -0,0 +1,14 @@
use osui::prelude::*;
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello World"
}
.view(&cx)
}
+19
View File
@@ -0,0 +1,19 @@
use osui::prelude::*;
pub fn main() {
let engine = Benchmark::new(Console::new());
let benchmark_result = engine.run(App {}).expect("Failed to run engine");
println!("Avg: {} μs", benchmark_result.average);
println!("Min: {} μs", benchmark_result.min);
println!("Max: {} μs", benchmark_result.max);
println!("Tot: {} μs", benchmark_result.total);
println!("Tot Render: {} μs", benchmark_result.total_render);
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello, world!"
}
.view(&cx)
}
+23
View File
@@ -0,0 +1,23 @@
use osui::prelude::*;
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run engine");
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MyComponent { "------ example" }
}
.view(&cx)
}
#[component]
fn MyComponent(cx: &Arc<Context>, children: &Rsx) -> View {
rsx! {
@{children}
"Simple"
}
.view(&cx)
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "osui-macros"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
description = "Macros for osui-rs"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
+183
View File
@@ -0,0 +1,183 @@
//! # RSX Emission
//!
//! Converts parsed RSX AST into Rust code that constructs RSX objects.
use crate::parse::*;
use proc_macro2::TokenStream;
use quote::quote;
/// Emits code for the root RSX
pub fn emit_rsx(root: RsxRoot) -> TokenStream {
emit_rsx_vec(&root.nodes)
}
/// Emits code for a vector of RSX nodes
pub fn emit_rsx_vec(nodes: &Vec<RsxNode>) -> TokenStream {
let nodes = nodes.iter().map(emit_node_scope);
quote! {{
let mut r = osui::frontend::Rsx::new();
#(#nodes)*
r
}}
}
/// Emits variable bindings for dependencies
fn emit_deps(deps: &[Dep]) -> TokenStream {
deps.iter()
.map(|d| {
let ident = &d.ident;
if let Some(pat) = &d.pat {
quote!( let #pat = #ident.clone(); )
} else {
quote!( let #ident = #ident.clone(); )
}
})
.collect()
}
/// Emits a Vec of dependencies as HookDependency trait objects
fn emit_deps_vec(deps: &[Dep]) -> TokenStream {
let deps = deps.iter().map(|d| {
let ident = &d.ident;
quote! {
std::sync::Arc::new(#ident) as std::sync::Arc<dyn HookDependency>
}
});
quote! {
vec![ #(#deps),* ]
}
}
/// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text(_) => {
let emit = emit_node(node);
quote! {
r.static_scope(move |scope| {#emit});
}
}
RsxNode::Component { .. } => {
let emit = emit_node(node);
quote! {
r.static_scope(move |scope| {#emit});
}
}
RsxNode::Mount(m) => quote! {
#m.mount();
},
RsxNode::If {
deps,
cond,
children,
} => {
let deps_emit = emit_deps(deps);
let deps_vec_emit = emit_deps_vec(deps);
let kids = children.iter().map(emit_node);
quote! {
{
#deps_emit
r.dynamic_scope(move |scope| {
if #cond {
if scope.children.lock().unwrap().is_empty() {
#(#kids)*
}
} else {
scope.children.lock().unwrap().clear();
}
}, #deps_vec_emit);
}
}
}
RsxNode::For {
deps,
pat,
expr,
children,
} => {
let deps_emit = emit_deps(deps);
let deps_vec_emit = emit_deps_vec(deps);
let kids = children.iter().map(emit_node);
quote! {
{
#deps_emit
#[allow(unused_parens)]
r.dynamic_scope(move |scope| {
scope.children.lock().unwrap().clear();
for #pat in #expr {
#(#kids)*
}
}, #deps_vec_emit);
}
}
}
RsxNode::Expr(expr) => quote! {
r.child(#expr);
},
}
}
fn emit_node(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text(text) => quote! {
scope.view(Arc::new(move |ctx| {
ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}));
},
RsxNode::Component {
path,
props,
children,
} => {
let prop_inits = props.iter().map(|p| {
let name = &p.name;
let value = &p.value;
quote! { #name: #value }
});
let emit_children = emit_rsx_vec(children);
let component_expr = if children.len() > 0 {
quote! {
#path {
#(#prop_inits,)*
children: #emit_children
}
}
} else {
quote! {
#path {
#(#prop_inits,)*
}
}
};
quote! {
scope.child(#component_expr, None);
}
}
RsxNode::Mount(m) => quote! {
#m.mount();
},
RsxNode::If { .. } => panic!("Invalid if statement"),
RsxNode::For { .. } => panic!("Invalid for loop"),
RsxNode::Expr(expr) => quote! {
r.child(#expr);
},
}
}
+149
View File
@@ -0,0 +1,149 @@
//! # OSUI Macros
//!
//! Procedural macros for OSUI that provide ergonomic syntax for defining components.
//!
//! ## Features
//!
//! - `#[component]` - Transforms a function into a reusable component with props
//! - `rsx!` - Creates RSX (React-like Syntax) for component hierarchies
use proc_macro::TokenStream;
use quote::quote;
use syn::{FnArg, ItemFn, Pat, ReturnType, Type, parse_macro_input};
mod emit;
mod parse;
/// RSX (React-like Syntax) macro for building component hierarchies
///
/// # Example
///
/// ```rust,ignore
/// rsx! {
/// Component {
/// prop: value,
/// }
/// }
/// ```
#[proc_macro]
pub fn rsx(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as parse::RsxRoot);
emit::emit_rsx(ast).into()
}
/// Component attribute macro for defining reusable components
///
/// Transforms a function into a component with automatic prop handling.
/// The first parameter must be `cx: &Arc<Context>`.
/// Remaining parameters become component props.
///
/// # Example
///
/// ```rust,ignore
/// #[component]
/// pub fn Counter(cx: &Arc<Context>, initial: &i32) -> View {
/// let count = use_state(*initial);
///
/// Arc::new(move |ctx| {
/// ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
/// })
/// }
/// ```
#[proc_macro_attribute]
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = &input.sig.ident;
let vis = &input.vis;
let body = &input.block;
let return_ty = match &input.sig.output {
ReturnType::Type(_, ty) => ty,
_ => {
return syn::Error::new_spanned(&input.sig, "component must return View")
.to_compile_error()
.into();
}
};
let mut inputs = input.sig.inputs.iter();
// ---- First param must be cx ----
let cx = match inputs.next() {
Some(FnArg::Typed(pat)) => pat,
_ => {
return syn::Error::new_spanned(&input.sig, "first argument must be cx: &Arc<Context>")
.to_compile_error()
.into();
}
};
let cx_ident = match &*cx.pat {
Pat::Ident(id) => &id.ident,
_ => unreachable!(),
};
let cx_ty = &cx.ty;
// ---- Remaining params are props ----
let mut struct_fields = Vec::new();
let mut render_params = Vec::new();
let mut call_args = Vec::new();
for arg in inputs {
let FnArg::Typed(pat) = arg else { continue };
let ident = match &*pat.pat {
Pat::Ident(id) => &id.ident,
_ => {
return syn::Error::new_spanned(pat, "unsupported prop pattern")
.to_compile_error()
.into();
}
};
// Strip leading &
let owned_ty = match &*pat.ty {
Type::Reference(r) => &r.elem,
ty => ty,
};
struct_fields.push(quote! {
pub #ident: #owned_ty
});
render_params.push(quote! {
#ident: &#owned_ty
});
call_args.push(quote! {
&self.#ident
});
}
let expanded = quote! {
#vis struct #name {
#(#struct_fields,)*
}
impl #name {
pub fn component(
#cx_ident: #cx_ty,
#(#render_params,)*
) -> #return_ty {
#body
}
}
impl ComponentImpl for #name {
fn call(&self, cx: &std::sync::Arc<Context>) -> #return_ty {
Self::component(
cx,
#(#call_args,)*
)
}
}
};
expanded.into()
}
+241
View File
@@ -0,0 +1,241 @@
//! # RSX Parser
//!
//! Parses RSX syntax into an AST that can be emitted as Rust code.
use syn::braced;
use syn::parse::discouraged::Speculative;
use syn::{
Expr, Ident, LitStr, Pat, Path, Result, Token,
parse::{Parse, ParseStream},
token::Brace,
};
/// Root of an RSX expression
pub struct RsxRoot {
/// Top-level nodes in the RSX
pub nodes: Vec<RsxNode>,
}
impl Parse for RsxRoot {
fn parse(input: ParseStream) -> Result<Self> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse()?);
}
Ok(Self { nodes })
}
}
/// A single component prop: `name: value`
pub struct RsxProp {
/// Property name
pub name: Ident,
/// Property value expression
pub value: Expr,
}
/// AST node representing different RSX constructs
pub enum RsxNode {
/// String literal: `"text"`
Text(LitStr),
/// Expression node: `{expr}`
Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }`
Component {
/// Component path (e.g., `my_module::MyComponent`)
path: Path,
/// Component properties
props: Vec<RsxProp>,
/// Child nodes
children: Vec<RsxNode>,
},
/// Mount lifecycle: `@mount`
Mount(Ident),
/// Conditional rendering: `@if condition { ... }`
If {
/// Dependencies to track for reactivity
deps: Vec<Dep>,
/// Condition expression
cond: Expr,
/// Child nodes to render if true
children: Vec<RsxNode>,
},
/// Loop rendering: `@for pattern in expr { ... }`
For {
/// Dependencies to track for reactivity
deps: Vec<Dep>,
/// Loop pattern (e.g., `(key, value)`)
pat: Pat,
/// Iterable expression
expr: Expr,
/// Child nodes to render for each iteration
children: Vec<RsxNode>,
},
}
pub struct Dep {
pub ident: Ident,
pub pat: Option<Pat>,
}
fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
let mut deps = Vec::new();
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
loop {
let ident: Ident = input.parse()?;
let pat = if input.peek(Token![as]) {
input.parse::<Token![as]>()?;
// Patterns in syn 2.0 are parsed via `Pat::parse_multi`
// instead of implementing the `Parse` trait directly.
Some(Pat::parse_multi(input)?)
} else {
None
};
deps.push(Dep { ident, pat });
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(deps)
}
impl Parse for RsxNode {
fn parse(input: ParseStream) -> Result<Self> {
// @{ $expr }
if input.peek(Token![@]) {
input.parse::<Token![@]>()?;
let content;
braced!(content in input);
let expr: Expr = content.parse()?;
return Ok(RsxNode::Expr(expr));
}
// !$ident
if input.peek(Token![!]) {
input.parse::<Token![!]>()?;
let mount: Ident = input.parse()?;
return Ok(RsxNode::Mount(mount));
}
let deps = parse_deps(input)?;
// %$dep if $expr { $rsx }
if input.peek(Token![if]) {
input.parse::<Token![if]>()?;
let cond: Expr = input.parse()?;
let content;
braced!(content in input);
let children = parse_children(&content)?;
return Ok(RsxNode::If {
deps,
cond,
children,
});
}
// %$dep for $pat in $expr { $rsx }
if input.peek(Token![for]) {
input.parse::<Token![for]>()?;
let pat: Pat = Pat::parse_multi(input)?;
input.parse::<Token![in]>()?;
let expr: Expr = input.parse()?;
let content;
braced!(content in input);
let children = parse_children(&content)?;
return Ok(RsxNode::For {
deps,
pat,
expr,
children,
});
}
// "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) {
return Ok(RsxNode::Text(input.parse()?));
}
parse_component_invocation(input)
}
}
/// Parses Path or Path { ... } into RsxNode::Component.
fn parse_component_invocation(input: ParseStream) -> Result<RsxNode> {
let path: Path = input.parse()?;
if !input.peek(Brace) {
return Ok(RsxNode::Component {
path,
props: Vec::new(),
children: Vec::new(),
});
}
let content;
braced!(content in input);
let (props, children) = parse_props_and_children(&content)?;
Ok(RsxNode::Component {
path,
props,
children,
})
}
fn parse_props_and_children(input: ParseStream) -> Result<(Vec<RsxProp>, Vec<RsxNode>)> {
Ok((parse_props(input)?, RsxRoot::parse(input)?.nodes))
}
fn parse_props(input: ParseStream) -> Result<Vec<RsxProp>> {
let mut props = Vec::new();
while !input.is_empty() {
let fork = input.fork();
let path: Path = match fork.parse() {
Ok(p) => p,
Err(_) => break,
};
if fork.peek(Token![:]) {
let name = path
.get_ident()
.cloned()
.ok_or_else(|| fork.error("prop name must be a single identifier"))?;
fork.parse::<Token![:]>()?;
let value: Expr = fork.parse()?;
input.advance_to(&fork);
props.push(RsxProp { name, value });
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
} else {
// ❌ do not commit → nothing consumed
break;
}
}
Ok(props)
}
fn parse_children(input: ParseStream) -> Result<Vec<RsxNode>> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse()?);
}
Ok(nodes)
}
+249
View File
@@ -0,0 +1,249 @@
//! # Context Module
//!
//! Provides the Context type which is central to component state management.
//! Context holds component state, manages event handlers, and coordinates
//! rendering and updates across the component tree.
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::{Arc, Mutex},
};
use access_cell::AccessCell;
use crate::{
component::EventHandler,
engine::{Command, CommandExecutor},
render::DrawContext,
state::{use_effect, HookDependency},
View,
};
use super::{scope::Scope, Component, ComponentImpl};
/// Context represents the runtime state and behavior of a component
///
/// Each component instance has a Context that holds:
/// - The component implementation
/// - The current view (render result)
/// - Event handlers for responding to events
/// - Child scopes for managing child components
pub struct Context {
/// The component implementation
component: AccessCell<Component>,
/// The current rendered view
view: AccessCell<View>,
/// Event handlers grouped by event type
event_handlers: AccessCell<HashMap<TypeId, Vec<EventHandler>>>,
/// Child scopes (component hierarchies)
pub(crate) scopes: Mutex<Vec<Arc<Scope>>>,
/// Command executor for this context's command handling
executor: Arc<dyn CommandExecutor>,
}
impl Context {
/// Creates a new context for the given component
pub fn new<F: ComponentImpl + 'static>(
component: F,
executor: Arc<dyn CommandExecutor>,
) -> Arc<Self> {
Arc::new(Self {
component: AccessCell::new(Arc::new(component)),
view: AccessCell::new(Arc::new(|_| {})),
event_handlers: AccessCell::new(HashMap::new()),
scopes: Mutex::new(Vec::new()),
executor,
})
}
/// Refreshes the component by re-rendering it
///
/// Clears event handlers and calls the component to produce a new view.
pub fn refresh(self: &Arc<Self>) {
self.event_handlers
.access(|event_handlers| event_handlers.clear());
self.component.access({
let s = self.clone();
move |component| {
let component = component.clone();
s.view.access({
let s = s.clone();
move |view| *view = component.call(&s)
})
}
});
}
/// Synchronously refreshes the component
///
/// Blocks until the component has finished rendering.
pub fn refresh_sync(self: &Arc<Self>) {
let (tx, rx) = std::sync::mpsc::channel::<()>();
self.event_handlers
.access(|event_handlers| event_handlers.clear());
self.component.access({
let s = self.clone();
move |component| {
let component = component.clone();
s.view.access({
let s = s.clone();
let tx = tx.clone();
move |view| {
*view = component.call(&s);
let _ = tx.send(()); // signal completion
}
});
}
});
// BLOCK until view closure finishes
let _ = rx.recv();
}
/// Gets the current view
pub fn get_view(self: &Arc<Self>) -> View {
self.view.access_ref().clone()
}
/// Registers an event handler for events of type T
///
/// When an event of type T is emitted, the handler is called with
/// the context and a reference to the event.
pub fn on_event<T: Any + 'static, F: Fn(&Arc<Self>, &T) + Send + Sync + 'static>(
self: &Arc<Self>,
handler: F,
) {
let new_handler: EventHandler =
Arc::new(Mutex::new(move |ctx: &Arc<Context>, event: &dyn Any| {
if let Some(e) = event.downcast_ref::<T>() {
(handler)(ctx, e);
}
}));
self.event_handlers.access(|event_handlers| {
event_handlers
.entry(TypeId::of::<T>())
.or_insert_with(Vec::new)
.push(new_handler);
});
}
/// Emits an event to this component and all descendants
///
/// Calls all registered handlers for this event type,
/// then propagates the event to child components.
pub fn emit_event<E: Send + Sync + Any + 'static>(self: &Arc<Self>, event: E) {
let event = Arc::new(event);
let handlers_to_call: Vec<EventHandler> = {
let guard = self.event_handlers.access_ref();
guard.get(&TypeId::of::<E>()).cloned().unwrap_or_default()
};
for h in &handlers_to_call {
(h.lock().unwrap())(self, event.as_ref());
}
for scope in self.scopes.lock().unwrap().iter() {
for (child, _) in scope.children.lock().unwrap().iter() {
child.emit_event(event.clone());
}
}
}
/// Emits an event to this component in a spawned thread
///
/// Similar to emit_event but handlers are called in spawned threads
/// for concurrent execution.
pub fn emit_event_threaded<E: Any + Send + Sync + Clone + 'static>(
self: &Arc<Self>,
event: &E,
) {
let handlers_to_call: Vec<EventHandler> = {
let guard = self.event_handlers.access_ref();
guard.get(&TypeId::of::<E>()).cloned().unwrap_or_default()
};
for h in handlers_to_call {
let event = event.clone();
let s = self.clone();
std::thread::spawn(move || {
(h.lock().unwrap())(&s, &event);
});
}
for scope in self.scopes.lock().unwrap().iter() {
for (child, _) in scope.children.lock().unwrap().iter() {
child.emit_event_threaded(event);
}
}
}
/// Creates a new child scope
pub fn scope(self: &Arc<Self>) -> Arc<Scope> {
let scope = Scope::new(self.executor.clone());
self.scopes.lock().unwrap().push(scope.clone());
scope
}
/// Creates a dynamic child scope that re-renders when dependencies change
pub fn dyn_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
self: &Arc<Self>,
drawer: F,
dependencies: &[&dyn HookDependency],
) -> Arc<Scope> {
let scope = Scope::new(self.executor.clone());
self.scopes.lock().unwrap().push(scope.clone());
drawer(&scope);
use_effect(
{
let scope = scope.clone();
move || {
drawer(&scope);
}
},
dependencies,
);
scope
}
/// Adds a pre-constructed scope as a child
pub fn add_scope(self: &Arc<Self>, scope: Arc<Scope>) {
self.scopes.lock().unwrap().push(scope);
}
/// Renders all child components to the draw context
pub fn draw_children(self: &Arc<Self>, ctx: &mut DrawContext) {
for scope in self.scopes.lock().unwrap().iter() {
for (child, view_wrapper) in scope.children.lock().unwrap().iter() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
view_wrapper(ctx, view)
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
}
/// Gets the command executor for this context
pub fn get_executor(self: &Arc<Self>) -> Arc<dyn CommandExecutor> {
self.executor.clone()
}
/// Executes a command
pub fn execute<T: Command + 'static>(self: &Arc<Self>, command: T) -> crate::Result<()> {
self.executor
.execute_command(&(Arc::new(command) as Arc<dyn Command>))
}
/// Stops the application
pub fn stop(self: &Arc<Self>) -> crate::Result<()> {
self.execute(crate::engine::commands::Stop)
}
}
+44
View File
@@ -0,0 +1,44 @@
//! # Component Module
//!
//! Provides the component system that forms the foundation of OSUI.
//! Components are reusable units of UI that can manage their own state
//! and respond to events.
pub mod context;
pub mod scope;
use std::{
any::Any,
sync::{Arc, Mutex},
};
use crate::View;
use context::Context;
/// A Component is an implementor of the ComponentImpl trait, wrapped in Arc
pub type Component = Arc<dyn ComponentImpl>;
/// An event handler function stored in a mutex for thread-safe mutation
pub type EventHandler = Arc<Mutex<dyn FnMut(&Arc<Context>, &dyn Any) + Send + Sync>>;
/// Trait implemented by components to render themselves
pub trait ComponentImpl: Send + Sync {
/// Renders the component within the given context, returning a View
fn call(&self, cx: &Arc<Context>) -> View;
}
impl ComponentImpl for View {
fn call(&self, _: &Arc<Context>) -> View {
self.clone()
}
}
impl<F> ComponentImpl for F
where
F: Fn(&Arc<Context>) -> View + Send + Sync,
{
fn call(&self, ctx: &Arc<Context>) -> View {
self(ctx)
}
}
+56
View File
@@ -0,0 +1,56 @@
//! # Scope Module
//!
//! Provides the Scope type for managing component hierarchies.
//! Scopes group child components and manage their lifecycle.
use std::sync::{Arc, Mutex};
use crate::{engine::CommandExecutor, View, ViewWrapper};
use super::{context::Context, ComponentImpl};
/// A scope groups child components and manages their rendering
///
/// Scopes form the hierarchical structure of a component tree.
/// Each scope contains references to its child components and their
/// optional view wrappers (for layout/styling).
pub struct Scope {
/// Child components with optional view wrappers
pub children: Mutex<Vec<(Arc<Context>, Option<ViewWrapper>)>>,
/// Command executor for this scope's children
executor: Arc<dyn CommandExecutor>,
}
impl Scope {
/// Creates a new scope with the given command executor
pub fn new(executor: Arc<dyn CommandExecutor>) -> Arc<Self> {
Arc::new(Self {
children: Mutex::new(Vec::new()),
executor,
})
}
/// Adds a child component to this scope
///
/// The view_wrapper is optional and can be used for layout or styling.
pub fn child<F: ComponentImpl + 'static>(
self: &Arc<Self>,
child: F,
view_wrapper: Option<ViewWrapper>,
) {
let ctx = Context::new(child, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, view_wrapper));
}
/// Adds a view directly to this scope
pub fn view(self: &Arc<Self>, view: View) {
let ctx = Context::new(view, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, None));
}
}
-70
View File
@@ -1,70 +0,0 @@
use std::sync::{Arc, Mutex};
use crate::{
widget::{Element, Widget},
NoRender,
};
pub struct Div {
color: u32,
children: Mutex<Vec<Arc<Widget>>>,
size: (u16, u16),
}
impl Element for Div {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.draw_rect(width, height, self.color);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
for elem in self.children.lock().unwrap().iter() {
scope.clear();
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.0.lock().unwrap().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(t.x + t.width);
transform.height = transform.height.max(t.y + t.height);
t.x += transform.x;
t.y += transform.y;
scope.draw();
elem.0.lock().unwrap().after_render(scope);
scope.clear();
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&self, element: &Arc<Widget>) {
self.children.lock().unwrap().push(element.clone());
element.inject(|w| w.component(NoRender));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Div {
pub fn new(color: u32) -> Self {
Div {
color,
children: Mutex::new(Vec::new()),
size: (0, 0),
}
}
}
-141
View File
@@ -1,141 +0,0 @@
use std::sync::{Arc, Mutex};
use crate::{widget::{Element, Widget}, NoRender};
pub struct FlexRow {
color: u32,
gap: u16,
children: Mutex<Vec<Arc<Widget>>>,
size: (u16, u16),
}
pub struct FlexCol {
color: u32,
gap: u16,
children: Mutex<Vec<Arc<Widget>>>,
size: (u16, u16),
}
impl Element for FlexRow {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.draw_rect(width, height, self.color);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
let mut v = 0;
for elem in self.children.lock().unwrap().iter() {
scope.clear();
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.0.lock().unwrap().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(t.width);
transform.height = transform.height.max(v + t.height);
t.x += transform.x;
t.y += transform.y + v;
v += t.height + self.gap;
scope.draw();
elem.0.lock().unwrap().after_render(scope);
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&self, element: &Arc<Widget>) {
self.children.lock().unwrap().push(element.clone());
element.inject(|w| w.component(NoRender));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl FlexRow {
pub fn new(color: u32, gap: u16) -> Self {
Self {
children: Mutex::new(Vec::new()),
size: (0, 0),
color,
gap,
}
}
}
impl Element for FlexCol {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let (width, height) = scope.get_size_or(self.size.0, self.size.1);
scope.draw_rect(width, height, self.color);
}
fn after_render(&mut self, scope: &mut crate::render_scope::RenderScope) {
let mut transform = scope.get_transform().clone();
let (w, h) = scope.get_parent_size();
scope.set_parent_size(transform.width, transform.height);
let mut v = 0;
for elem in self.children.lock().unwrap().iter() {
scope.clear();
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
elem.0.lock().unwrap().render(scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
let t = scope.get_transform_mut();
transform.width = transform.width.max(v + t.width);
transform.height = transform.height.max(t.height);
t.x += transform.x + v;
t.y += transform.y;
v += t.width + self.gap;
scope.draw();
elem.0.lock().unwrap().after_render(scope);
}
scope.set_parent_size(w, h);
self.size = (transform.width, transform.height);
}
fn draw_child(&self, element: &Arc<Widget>) {
self.children.lock().unwrap().push(element.clone());
element.inject(|w| w.component(NoRender));
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl FlexCol {
pub fn new(color: u32, gap: u16) -> Self {
Self {
children: Mutex::new(Vec::new()),
size: (0, 0),
color,
gap,
}
}
}
-37
View File
@@ -1,37 +0,0 @@
pub mod div;
pub mod flex;
pub mod shape;
pub use div::*;
pub use flex::*;
pub use shape::*;
use crate::Element;
impl Element for String {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
scope.draw_text(self);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Element for (String, u32) {
fn render(&mut self, scope: &mut crate::render_scope::RenderScope) {
scope.draw_text_colored(&self.0, self.1);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
-59
View File
@@ -1,59 +0,0 @@
use crate::widget::Element;
pub struct RoundedOutline;
impl Element for RoundedOutline {
fn render(&mut self, scope: &mut crate::prelude::RenderScope) {
let (w, h) = scope.get_size_or_parent();
if w > 2 && h > 2 {
let d = "─".repeat(w as usize - 2);
scope.draw_text(&format!(
"╭{d}╮{}\n╰{d}╯",
format!("\n│{}│", " ".repeat(w as usize - 2)).repeat(h as usize - 2),
));
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl RoundedOutline {
pub fn new() -> Self {
Self
}
}
pub struct Outline;
impl Element for Outline {
fn render(&mut self, scope: &mut crate::prelude::RenderScope) {
let (w, h) = scope.get_size_or_parent();
if w > 2 && h > 2 {
let d = "─".repeat(w as usize - 2);
scope.draw_text(&format!(
"┌{d}┐{}\n└{d}┘",
format!("\n│{}│", " ".repeat(w as usize - 2)).repeat(h as usize - 2),
));
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl Outline {
pub fn new() -> Self {
Self
}
}
+110
View File
@@ -0,0 +1,110 @@
//! # Benchmark Module
//!
//! Provides performance benchmarking capabilities for rendering engines.
use std::{io::stdout, sync::Arc, time::Instant};
use crossterm::{cursor::MoveTo, execute, terminal::Clear};
use crate::component::{context::Context, ComponentImpl};
use crate::{render::Area, DrawContext, View};
use super::Engine;
/// Results of a benchmark run
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
/// Average render time in microseconds
pub average: u128,
/// Minimum render time in microseconds
pub min: u128,
/// Maximum render time in microseconds
pub max: u128,
/// Total time spent rendering in microseconds
pub total_render: u128,
/// Total benchmark time including setup in microseconds
pub total: u128,
}
/// Wraps an engine to benchmark its rendering performance
pub struct Benchmark<T: Engine>(T);
impl<T: Engine> Benchmark<T> {
/// Creates a new benchmark wrapper around the given engine
pub fn new(engine: T) -> Self {
Self(engine)
}
}
impl<T: Engine> Engine<BenchmarkResult> for Benchmark<T> {
fn run<F: ComponentImpl + 'static>(&self, component: F) -> crate::Result<BenchmarkResult> {
let mut times: Vec<u128> = Vec::new();
let cx = self.init(component);
let start = Instant::now();
// Run 40 render cycles and measure each
for _ in 0..40 {
let start = Instant::now();
self.render(&cx);
let end = Instant::now();
times.push(end.duration_since(start).as_micros());
}
let end = Instant::now();
execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap();
execute!(stdout(), MoveTo(0, 0)).unwrap();
Ok(BenchmarkResult {
min: *times.iter().min().unwrap_or(&0),
max: *times.iter().max().unwrap_or(&0),
total: end.duration_since(start).as_micros(),
total_render: times.iter().sum::<u128>(),
average: if times.len() > 0 {
times.iter().sum::<u128>() / (times.len() as u128)
} else {
0
},
})
}
fn init<F: ComponentImpl + 'static>(&self, component: F) -> Arc<Context> {
self.0.init(component)
}
fn draw_context(&self, ctx: &DrawContext) {
self.0.draw_context(ctx)
}
fn render(&self, cx: &Arc<Context>) {
self.0.render(cx)
}
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
self.0.render_view(area, view)
}
fn render_delay(&self) {
self.0.render_delay();
}
fn executor(&self) -> Arc<dyn super::CommandExecutor> {
self.0.executor()
}
}
impl std::fmt::Display for BenchmarkResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"- Average: {} µs\n\
- Min: {} µs\n\
- Max: {} µs\n\
- Total Render: {} µs\n\
- Total: {} µs",
self.average, self.min, self.max, self.total_render, self.total
)
}
}
+14
View File
@@ -0,0 +1,14 @@
//! # Commands Module
//!
//! Defines built-in commands for controlling the engine.
use crate::engine::Command;
/// Command to stop the engine and terminate the application
pub struct Stop;
impl Command for Stop {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
+153
View File
@@ -0,0 +1,153 @@
//! # Console Engine Implementation
//!
//! Provides a Console implementation of the Engine trait for rendering
//! to the terminal using crossterm.
use std::{
io::{stdout, Write},
sync::{Arc, Mutex},
};
use crossterm::{cursor::MoveTo, execute, terminal::Clear};
use crate::component::{context::Context, ComponentImpl};
use crate::{
engine::{commands, CommandExecutor},
render::Area,
DrawContext, View,
};
use super::Engine;
/// Executes commands for the console engine
pub struct ConsoleExecutor {
/// Flag indicating whether the application is running
running: Mutex<bool>,
}
/// Console-based rendering engine
///
/// Renders components to the terminal using crossterm for cross-platform support.
pub struct Console {
/// Thread functions to execute
threads: Mutex<Vec<Arc<dyn Fn(Arc<Context>) + Send + Sync>>>,
/// The executor for this console
executor: Arc<ConsoleExecutor>,
}
impl Console {
/// Creates a new console engine
pub fn new() -> Self {
Self {
threads: Mutex::new(Vec::new()),
executor: Arc::new(ConsoleExecutor {
running: Mutex::new(true),
}),
}
}
/// Registers a thread function to run alongside the engine
pub fn thread<F: Fn(Arc<Context>) + Send + Sync + 'static>(&self, run: F) {
self.threads.lock().unwrap().push(Arc::new(run));
}
}
impl Engine for Console {
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
let mut context = DrawContext::new(area.clone());
view(&mut context);
context
}
fn draw_context(&self, ctx: &DrawContext) {
for inst in &ctx.drawing {
match inst {
crate::render::DrawInstruction::Text(point, text) => {
let (x, y) = (ctx.area.x + point.x, ctx.area.y + point.y);
execute!(stdout(), MoveTo(x, y),).unwrap();
print!("{text}");
stdout().flush().unwrap();
}
crate::render::DrawInstruction::Child(_point, child) => self.draw_context(child),
crate::render::DrawInstruction::View(area, view) => {
self.draw_context(&self.render_view(area, view))
}
}
}
}
fn render(&self, cx: &Arc<Context>) {
let (width, height) = crossterm::terminal::size().unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap();
execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap();
self.draw_context(&self.render_view(
&Area {
x: 0,
y: 0,
width,
height,
},
&cx.get_view(),
));
}
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context> {
crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).unwrap();
let cx = Context::new(component, self.executor.clone());
cx.refresh();
for thread in self.threads.lock().unwrap().iter() {
let thread = thread.clone();
std::thread::spawn({
let cx = cx.clone();
move || thread(cx)
});
}
cx
}
fn executor(&self) -> Arc<dyn super::CommandExecutor> {
self.executor.clone()
}
fn run<F: ComponentImpl + 'static>(&self, component: F) -> crate::Result<()> {
let cx = self.init(component);
while self.executor.is_running() {
self.render(&cx);
self.render_delay();
}
Ok(())
}
}
impl ConsoleExecutor {
/// Checks if the engine is still running
pub fn is_running(self: &Arc<ConsoleExecutor>) -> bool {
*self.running.lock().unwrap()
}
/// Stops the engine
pub fn stop(&self) -> crate::Result<()> {
*self.running.lock()? = false;
Ok(())
}
}
impl CommandExecutor for ConsoleExecutor {
fn execute_command(&self, command: &Arc<dyn super::Command>) -> crate::Result<()> {
let command = command.as_any();
if let Some(commands::Stop) = command.downcast_ref() {
self.stop()?;
}
Ok(())
}
}
+55
View File
@@ -0,0 +1,55 @@
//! # Engine Module
//!
//! Provides the rendering engine and command execution system.
//! The engine is responsible for initializing components, rendering frames,
//! and handling user commands.
pub mod benchmark;
pub mod commands;
pub mod console;
pub use benchmark::*;
pub use console::*;
use std::{any::Any, sync::Arc};
use crate::component::{context::Context, ComponentImpl};
use crate::{render::Area, DrawContext, View};
/// Main engine trait for rendering and running components
pub trait Engine<Output = ()> {
/// Runs a component to completion
fn run<C: ComponentImpl + 'static>(&self, component: C) -> crate::Result<Output>;
/// Initializes a component and returns its context
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context>;
/// Renders the current state of a component
fn render(&self, cx: &Arc<Context>);
/// Sleeps between render frames (default 16ms for ~60fps)
fn render_delay(&self) {
crate::sleep(16);
}
/// Renders a view within an area and returns the draw context
fn render_view(&self, area: &Area, view: &View) -> DrawContext;
/// Executes the drawing instructions in a draw context
fn draw_context(&self, ctx: &DrawContext);
/// Returns the command executor for this engine
fn executor(&self) -> Arc<dyn CommandExecutor>;
}
/// Trait for commands that can be executed by the engine
pub trait Command {
/// Returns the command as Any for downcasting
fn as_any(&self) -> &dyn Any;
}
/// Executes commands during the application lifecycle
pub trait CommandExecutor: Send + Sync {
/// Executes the given command
fn execute_command(&self, command: &Arc<dyn Command>) -> crate::Result<()>;
}
-27
View File
@@ -1,27 +0,0 @@
use std::sync::Arc;
use crate::{component, extensions::Extension, widget::Widget, Screen};
pub struct IdExtension(pub Arc<Screen>);
impl Extension for Arc<IdExtension> {}
impl IdExtension {
pub fn new(screen: Arc<Screen>) -> Arc<Self> {
Arc::new(IdExtension(screen))
}
pub fn get_element(self: &Arc<IdExtension>, id: usize) -> Option<Arc<Widget>> {
for elem in self.0.widgets.lock().unwrap().iter() {
if let Some(current_id) = elem.get::<Id>() {
if current_id.0 == id {
return Some(elem.clone());
}
}
}
None
}
}
component!(Id(pub usize));
-59
View File
@@ -1,59 +0,0 @@
pub mod id;
pub mod resources;
pub mod tick;
pub mod velocity;
pub use id::*;
pub use resources::*;
pub use tick::*;
pub use velocity::*;
use std::{
any::{type_name, Any},
fmt::Debug,
sync::{Arc, Mutex},
};
use crate::{
render_scope::RenderScope,
widget::{Component, Widget},
Screen,
};
pub trait Extension {
fn init(&mut self, _screen: Arc<Screen>) {}
fn render_widget(&mut self, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
}
pub trait Event: Send + Sync {
fn as_any(&self) -> &dyn Any;
}
#[derive(Clone)]
pub struct Handler<E: Event>(Arc<Mutex<dyn FnMut(&Arc<Widget>, &E) + Send + Sync>>);
impl<E: Event + 'static> Component for Handler<E> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl<E: Event + 'static> Handler<E> {
pub fn new<F: FnMut(&Arc<Widget>, &E) + Send + Sync + 'static>(f: F) -> Handler<E> {
Handler(Arc::new(Mutex::new(f)))
}
pub fn call(&self, w: &Arc<Widget>, e: &E) {
(self.0.lock().unwrap())(w, e)
}
}
impl<E: Event> Debug for Handler<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Handler({})", type_name::<E>())
}
}
-22
View File
@@ -1,22 +0,0 @@
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct VecResource<T>(Arc<Mutex<Box<dyn FnMut(&T) + Send + Sync>>>);
impl<T: Send + Sync + 'static> VecResource<T> {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(Box::new(|_| {}))))
}
pub fn iterate<F: FnMut(&T) + Send + Sync + 'static>(&self, f: F) {
*self.0.lock().unwrap() = Box::new(f);
}
pub fn push(&self, e: T) {
let i = self.0.clone();
std::thread::spawn(move || {
(i.lock().unwrap())(&e);
});
}
}
-29
View File
@@ -1,29 +0,0 @@
use std::{fmt::Debug, sync::Arc};
use crate::extensions::Extension;
use crate::extensions::Handler;
use crate::{event, Screen};
pub struct TickExtension(pub u16);
impl Extension for TickExtension {
fn init(&mut self, screen: Arc<Screen>) {
let rate_dur = 1000 / self.0 as u64;
std::thread::spawn({
move || {
let mut tick = 0;
loop {
for w in screen.widgets.lock().unwrap().iter() {
if let Some(on_tick) = w.get::<Handler<TickEvent>>() {
on_tick.call(&w, &TickEvent(tick))
}
}
tick += 1;
std::thread::sleep(std::time::Duration::from_millis(rate_dur));
}
}
});
}
}
event!(TickEvent(pub u32));
-66
View File
@@ -1,66 +0,0 @@
use std::sync::Arc;
use crate::{
component,
extensions::Extension,
style::{Position, Transform},
widget::Widget,
Screen,
};
pub struct VelocityExtension;
impl VelocityExtension {
fn apply_velocity(ticks: u16, velocity: i32, x: &mut u16) {
if velocity.abs() != 0 && ticks as i32 % (1000 / velocity.abs()) == 0 {
if velocity > 0 {
*x += 1;
} else if velocity < 0 {
*x -= 1;
}
}
}
}
impl VelocityExtension {
fn apply_velocity_xy(ticks: u16, widget: &Arc<Widget>) {
if let Some(velocity) = widget.get::<Velocity>() {
if let Some(mut t) = widget.get::<Transform>() {
match &mut t.x {
Position::Const(x) => Self::apply_velocity(ticks, velocity.0, x),
_ => {}
}
match &mut t.y {
Position::Const(y) => Self::apply_velocity(ticks, velocity.1, y),
_ => {}
}
widget.set_component(t);
}
}
}
}
impl Extension for VelocityExtension {
fn init(&mut self, screen: Arc<Screen>) {
std::thread::spawn({
move || {
let mut tick = 0;
loop {
for widget in screen.widgets.lock().unwrap().iter() {
Self::apply_velocity_xy(tick, widget);
}
if tick > 1000 {
tick = 0;
} else {
tick += 1;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
});
}
}
component!(Velocity(pub i32, pub i32));
+104 -40
View File
@@ -1,58 +1,122 @@
//! # Frontend Module
//!
//! Provides the RSX (React-like Syntax) system for composing components.
//! This module defines the structure for building component hierarchies
//! with static and dynamic scopes, similar to React's JSX.
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::component::{context::Context, scope::Scope};
state::DependencyHandler, use crate::{render::Point, state::HookDependency, View};
widget::{Widget, WidgetLoad},
Screen,
};
pub enum RsxElement { /// Trait for converting values to RSX
Element( pub trait ToRsx {
Box<dyn FnMut() -> WidgetLoad + Send + Sync>, /// Convert to RSX representation
Vec<Box<dyn DependencyHandler>>, fn to_rsx(&self) -> Rsx;
Rsx,
),
} }
pub struct Rsx(pub Vec<RsxElement>); /// Scope types for RSX components
#[derive(Clone)]
pub enum RsxScope {
/// Static scope - executed once and never updated
Static(Arc<dyn Fn(&Arc<Scope>) + Send + Sync>),
/// Dynamic scope - re-executed when dependencies change
Dynamic(
Arc<dyn Fn(&Arc<Scope>) + Send + Sync>,
Vec<Arc<dyn HookDependency>>,
),
/// Child RSX scope for composition
Child(Rsx),
}
/// RSX (React-like Syntax) builder for component hierarchies
///
/// Represents a collection of scopes that define component structure.
/// Scopes can be static (execute once) or dynamic (reactive to changes).
#[derive(Clone)]
pub struct Rsx(Vec<RsxScope>);
impl Rsx { impl Rsx {
pub fn draw(self, screen: &Arc<Screen>) { /// Creates a new empty RSX
self.draw_parent(screen, None); pub fn new() -> Self {
Self(Vec::new())
} }
pub fn draw_parent(self, screen: &Arc<Screen>, parent: Option<Arc<Widget>>) { /// Adds a static scope to this RSX
for rsx_elem in self.0 { ///
match rsx_elem { /// The provided function is executed once during rendering
RsxElement::Element(f, dep, child) => { /// and will not be re-executed on dependency changes.
let w = if let Some(parent) = &parent { pub fn static_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(&mut self, scope: F) {
let w = screen.draw_box(f); self.0.push(RsxScope::Static(Arc::new(scope)));
parent.0.lock().unwrap().draw_child(&w);
w
} else {
screen.draw_box(f)
};
for d in dep {
w.dependency_box(d);
}
child.draw_parent(screen, Some(w.clone()));
}
}
}
} }
pub fn create_element<F: FnMut() -> WidgetLoad + Send + Sync + 'static>( /// Adds a dynamic scope to this RSX
///
/// The provided function is executed when dependencies change,
/// allowing for reactive updates similar to React hooks.
pub fn dynamic_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(
&mut self, &mut self,
load: F, drawer: F,
dependencies: Vec<Box<dyn DependencyHandler>>, dependencies: Vec<Arc<dyn HookDependency>>,
children: Rsx,
) { ) {
self.0 self.0
.push(RsxElement::Element(Box::new(load), dependencies, children)); .push(RsxScope::Dynamic(Arc::new(drawer), dependencies));
} }
pub fn expand(&mut self, other: &mut Rsx) { /// Adds a child RSX
self.0.append(&mut other.0); pub fn child<R: ToRsx>(&mut self, child: R) {
self.0.push(RsxScope::Child(child.to_rsx()));
}
/// Generates child components within the given context
pub fn generate_children(&self, context: &Arc<Context>) {
let executor = context.get_executor();
for scope in &self.0 {
match scope {
RsxScope::Static(scope_fn) => {
let scope = Scope::new(executor.clone());
(scope_fn)(&scope);
context.add_scope(scope)
}
RsxScope::Dynamic(drawer, dependencies) => {
let drawer = drawer.clone();
context.dyn_scope(
move |c| drawer(c),
&dependencies.iter().map(|d| d.as_ref()).collect::<Vec<_>>(),
);
}
RsxScope::Child(child) => child.generate_children(context),
}
}
}
/// Converts this RSX to a View
pub fn view(&self, context: &Arc<Context>) -> View {
let context = context.clone();
self.generate_children(&context);
Arc::new({
move |ctx| {
context.draw_children(ctx);
}
})
}
}
impl ToRsx for &Rsx {
fn to_rsx(&self) -> Rsx {
Rsx(self.0.clone())
}
}
impl<T: std::fmt::Display> ToRsx for T {
fn to_rsx(&self) -> Rsx {
let s = self.to_string();
Rsx(vec![RsxScope::Static(Arc::new(move |scope| {
let s = s.clone();
scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &s)))
}))])
} }
} }
+76 -118
View File
@@ -1,132 +1,90 @@
use std::sync::{Arc, Mutex}; //! # OSUI - A TUI Library for Advanced UIs
//!
//! OSUI is a Rust library for building sophisticated Terminal User Interfaces (TUIs).
//! It provides a component-based architecture with state management, event handling,
//! and rendering capabilities for creating interactive console applications.
//!
//! ## Key Features
//!
//! - **Component System**: Build UIs using composable components
//! - **State Management**: React-like hooks for managing component state
//! - **Event Handling**: Type-safe event system with reactive updates
//! - **RSX Syntax**: Macro-based DSL for defining component hierarchies
//! - **Console Engine**: Terminal rendering with crossterm support
//!
//! ## Architecture
//!
//! - [`component`] - Component system and context management
//! - [`state`] - State management with hooks (useState, useEffect, etc.)
//! - [`engine`] - Rendering engine and command execution
//! - [`frontend`] - RSX (React-like Syntax) for component definitions
//! - [`render`] - Low-level rendering primitives
//!
//! ## Example
//!
//! ```rust,no_run
//! use osui::prelude::*;
//! use std::sync::Arc;
//!
//! #[component]
//! pub fn Counter(cx: &Arc<Context>) -> View {
//! let count = use_state(0);
//!
//! Arc::new(move |ctx| {
//! ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
//! })
//! }
//! ```
use crate::{ use std::sync::Arc;
extensions::{Extension, Handler},
render_scope::RenderScope,
widget::{Element, Widget, WidgetLoad},
};
pub mod elements; use crate::render::DrawContext;
pub mod extensions;
pub mod component;
pub mod engine;
pub mod frontend; pub mod frontend;
pub mod macros; pub mod render;
pub mod render_scope;
pub mod state; pub mod state;
pub mod style;
pub mod utils;
pub mod widget;
pub mod prelude { pub mod prelude {
pub use crate::{ //! Prelude module - Re-exports commonly used items for convenience
elements::*, extensions::*, frontend::*, render_scope::*, state::*, style::*, utils::*, pub use crate::component::{context::*, scope::*, *};
widget::*, *, pub use crate::engine::*;
}; pub use crate::frontend::*;
pub use crate::render::*;
pub use crate::style::{Dimension::{Full, Content}, Position::{Center, End}}; pub use crate::state::*;
pub use crate::{sleep, Error, Result, View, ViewWrapper};
pub use crossterm;
pub use osui_macros::{component, rsx};
pub use std::sync::{Arc, Mutex};
} }
pub struct Screen { /// A View is an async function that renders content to a DrawContext.
pub widgets: Mutex<Vec<Arc<Widget>>>, /// It takes a mutable DrawContext and produces drawing instructions.
extensions: Mutex<Vec<Arc<Mutex<Box<dyn Extension + Send + Sync>>>>>, pub type View = Arc<dyn Fn(&mut DrawContext) + Send + Sync>;
/// A ViewWrapper is a higher-order function that wraps views.
/// It can modify or enhance how a view is rendered.
pub type ViewWrapper = Arc<dyn Fn(&mut DrawContext, View) + Send + Sync>;
/// Result type for OSUI operations
pub type Result<T> = std::result::Result<T, Error>;
/// Error type for OSUI operations
#[derive(Debug, Clone)]
pub enum Error {
/// Error that occurs when a mutex is poisoned
PoisonError,
} }
event!(RenderWrapperEvent(*mut RenderScope)); impl From<std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>> for Error {
component!(NoRender); fn from(_value: std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>) -> Self {
Error::PoisonError
impl RenderWrapperEvent {
pub fn get_scope(&self) -> &mut RenderScope {
unsafe { &mut *self.0 }
} }
} }
unsafe impl Send for RenderWrapperEvent {} /// Sleep for the specified duration in milliseconds.
unsafe impl Sync for RenderWrapperEvent {} /// Useful for controlling render frame rate or delays.
pub fn sleep(delay_ms: u64) {
impl Screen { std::thread::sleep(std::time::Duration::from_millis(delay_ms));
pub fn new() -> Arc<Self> {
Arc::new(Self {
widgets: Mutex::new(Vec::new()),
extensions: Mutex::new(Vec::new()),
})
}
pub fn draw<F: FnMut() -> WidgetLoad + 'static + Send + Sync>(
self: &Arc<Self>,
element: F,
) -> Arc<Widget> {
let w = Arc::new(Widget::new(Box::new(element)));
self.widgets.lock().unwrap().push(w.clone());
w
}
pub fn draw_box(
self: &Arc<Self>,
element: Box<dyn FnMut() -> WidgetLoad + Send + Sync>,
) -> Arc<Widget> {
let w = Arc::new(Widget::new(Box::new(element)));
self.widgets.lock().unwrap().push(w.clone());
w
}
pub fn extension<E: Extension + Send + Sync + 'static>(self: &Arc<Self>, ext: E) {
self.extensions
.lock()
.unwrap()
.push(Arc::new(Mutex::new(Box::new(ext))));
}
pub fn run(self: &Arc<Self>) -> std::io::Result<()> {
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().init(self.clone());
}
utils::hide_cursor()?;
loop {
self.render()?;
std::thread::sleep(std::time::Duration::from_millis(28));
}
}
pub fn render(self: &Arc<Self>) -> std::io::Result<()> {
let mut scope = RenderScope::new();
let (w, h) = crossterm::terminal::size().unwrap();
scope.set_parent_size(w, h);
utils::clear()?;
for elem in self.widgets.lock().unwrap().iter() {
if let Some(NoRender) = elem.get() {
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().render_widget(&mut scope, elem);
}
elem.auto_refresh();
continue;
}
if let Some(wrapper) = elem.get::<Handler<RenderWrapperEvent>>() {
wrapper.call(elem, &RenderWrapperEvent(&mut scope));
} else {
scope.clear();
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
for ext in self.extensions.lock().unwrap().iter() {
ext.lock().unwrap().render_widget(&mut scope, elem);
}
elem.0.lock().unwrap().render(&mut scope);
if let Some(t) = elem.get() {
scope.set_transform(&t);
}
scope.draw();
elem.0.lock().unwrap().after_render(&mut scope);
}
elem.auto_refresh();
}
Ok(())
}
} }
-175
View File
@@ -1,175 +0,0 @@
#[macro_export]
macro_rules! event {
($name:ident) => {
#[derive(Debug, Clone)]
pub struct $name;
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
($name:ident {$($inner:tt)*}) => {
#[derive(Debug, Clone)]
pub struct $name {
$($inner)*
}
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
($name:ident ($($inner:tt)*)) => {
#[derive(Debug, Clone)]
pub struct $name ($($inner)*);
impl $crate::extensions::Event for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
}
#[macro_export]
macro_rules! component {
($name:ident) => {
#[derive(Debug, Clone)]
pub struct $name;
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
($name:ident {$($inner:tt)*}) => {
#[derive(Debug, Clone)]
pub struct $name {
$($inner)*
}
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
($name:ident ($($inner:tt)*)) => {
#[derive(Debug, Clone)]
pub struct $name ($($inner)*);
impl $crate::widget::Component for $name {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
}
/// Macro to create an event handler closure that calls a method on `self`
///
/// This macro automates the pattern of capturing `self` as a raw pointer,
/// then passing an event to a method on `self` inside an unsafe closure.
///
/// # Parameters
/// - `$self_ty`: The self type.
/// - `$self`: The instance variable (usually `self`) to call the method on.
/// - `$events`: The event source which has an `.on` method to register the handler.
/// - `$method`: The method name on `self` to call when an event occurs.
///
/// # Usage
/// ```rust
/// event_handler!(self, events, on_keypress);
/// ```
///
/// This expands roughly to:
/// ```rust
/// let self_ref = self as *mut Self;
/// events.on(move |event| unsafe { (*self_ref).on_keypress(event) });
/// ```
///
/// # Safety
/// This macro uses `unsafe` code because it dereferences a raw pointer inside the closure.
/// Ensure that the `self` reference lives at least as long as the closure to avoid undefined behavior.
///
/// # Why use raw pointers here?
/// Often, event handlers require `'static` closures, but `self` is a stack reference.
/// Capturing `self` directly is not possible, so this workaround uses a raw pointer.
#[macro_export]
macro_rules! event_handler {
($self_ty:ty, $self:ident, $events:ident, $method:ident) => {{
let self_ref = $self as *mut $self_ty;
$events.on(move |es, e| unsafe { (*self_ref).$method(es, e) });
}};
}
#[macro_export]
macro_rules! transform {
($($f:ident: $v:expr),* $(,)?) => {{
let mut t = Transform::new();
$(t.$f = $v.into();)*
t
}};
}
#[macro_export]
macro_rules! rsx {
($($inner:tt)*) => {{
let mut r = $crate::frontend::Rsx(Vec::new());
$crate::rsx_inner! { r, $($inner)* };
r
}};
}
#[macro_export]
macro_rules! rsx_inner {
($r:expr, $(%$dep:ident)* $(@$comp:expr;)* $s:literal $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new(format!($s)) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::frontend::Rsx(Vec::new()));
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(%$dep:ident)* $(@$comp:expr;)* $name:path { $($inner:tt)* } ($($e:expr),*) $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new(<$name>::new($($e),*)) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $(%$dep:ident)* $(@$comp:expr;)* $name:path { $($inner:tt)* } $($rest:tt)*) => {
$r.create_element({ $(let $dep = $dep.clone();)* move || {
$crate::widget::WidgetLoad::new(<$name>::new()) $(.component($comp))*
} }, vec![$(Box::new($dep.clone())),*], $crate::rsx!{ $($inner)* });
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr, $expand:ident => ($($inner:tt)*) $($rest:tt)*) => {
$r.expand(&mut $expand($($inner)*));
$crate::rsx_inner! { $r, $($rest)* };
};
($r:expr,) => {};
}
-28
View File
@@ -1,28 +0,0 @@
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
app().draw(&screen);
screen.run()
}
fn app() -> Rsx {
rsx! {
@transform! {
width: Full,
height: Full,
};
Div {
@transform! {
width: Full,
height: Full,
};
RoundedOutline {}
@Transform::center();
"Welcome To OSUI!"
} (0x000000)
}
}
+111
View File
@@ -0,0 +1,111 @@
//! # Rendering Module
//!
//! This module provides low-level rendering primitives and data structures
//! for drawing content to the terminal. It includes geometric primitives
//! (Point, Area, Size) and drawing instructions.
use crate::View;
/// Represents a drawing instruction that can be executed by the rendering engine
#[derive(Clone)]
pub enum DrawInstruction {
/// Draw text at a specific point
Text(Point, String),
/// Render a view within a specified area
View(Area, View),
/// Render a child drawing context at an offset
Child(Point, DrawContext),
}
/// Represents the dimensions of a drawable area
#[derive(Clone)]
pub struct Size {
/// Width in terminal columns
pub width: u16,
/// Height in terminal rows
pub height: u16,
}
/// Represents a position in 2D space
#[derive(Clone)]
pub struct Point {
/// X coordinate (column)
pub x: u16,
/// Y coordinate (row)
pub y: u16,
}
/// Represents a rectangular area with position and dimensions
#[derive(Clone)]
pub struct Area {
/// X coordinate (column) of the top-left corner
pub x: u16,
/// Y coordinate (row) of the top-left corner
pub y: u16,
/// Width in terminal columns
pub width: u16,
/// Height in terminal rows
pub height: u16,
}
/// Context for drawing operations
///
/// Accumulates drawing instructions that are executed by the rendering engine.
/// Tracks allocated space within the drawable area.
#[derive(Clone)]
pub struct DrawContext {
/// The total area available for drawing
pub area: Area,
/// The area that has been allocated for drawing (union of all allocations)
pub allocated: Area,
/// List of drawing instructions to execute
pub drawing: Vec<DrawInstruction>,
}
impl DrawContext {
/// Creates a new DrawContext with the specified area
pub fn new(area: Area) -> Self {
Self {
area,
allocated: Area {
x: u16::MAX,
y: u16::MAX,
width: 0,
height: 0,
},
drawing: Vec::new(),
}
}
/// Allocates space within the drawable area and returns the allocated area
/// Updates the allocated bounds to include this allocation
pub fn allocate(&mut self, x: u16, y: u16, width: u16, height: u16) -> Area {
self.allocated.x = self.allocated.x.min(x);
self.allocated.y = self.allocated.y.min(y);
self.allocated.width = self.allocated.width.max(width);
self.allocated.height = self.allocated.height.max(height);
Area {
x,
y,
width,
height,
}
}
/// Adds a drawing instruction to be executed
pub fn draw(&mut self, inst: DrawInstruction) {
self.drawing.push(inst);
}
/// Draws text at the specified point
pub fn draw_text(&mut self, point: Point, text: &str) {
self.drawing
.push(DrawInstruction::Text(point, text.to_string()));
}
/// Draws a view within the specified area
pub fn draw_view(&mut self, area: Area, view: View) {
self.drawing.push(DrawInstruction::View(area, view));
}
}
-154
View File
@@ -1,154 +0,0 @@
use std::fmt::Debug;
use crate::{
style::{RawTransform, Transform},
utils::{self, hex_ansi_bg},
};
#[derive(Clone)]
enum RenderMethod {
Text(String),
TextColored(String, u32),
Rectangle(u16, u16, u32),
}
#[derive(Clone)]
pub struct RenderScope {
transform: RawTransform,
render_stack: Vec<RenderMethod>,
parent_width: u16,
parent_height: u16,
}
impl RenderScope {
pub fn new() -> RenderScope {
RenderScope {
transform: RawTransform::new(),
render_stack: Vec::new(),
parent_width: 0,
parent_height: 0,
}
}
pub fn set_transform_raw(&mut self, transform: RawTransform) {
self.transform = transform;
}
pub fn set_transform(&mut self, transform: &Transform) {
transform.use_dimensions(self.parent_width, self.parent_height, &mut self.transform);
transform.use_position(self.parent_width, self.parent_height, &mut self.transform);
}
pub fn draw_text(&mut self, text: &str) {
self.render_stack.push(RenderMethod::Text(text.to_string()));
let (w, h) = utils::str_size(text);
self.transform.width = self.transform.width.max(w);
self.transform.height = self.transform.height.max(h);
}
pub fn draw_text_colored(&mut self, text: &str, color: u32) {
self.render_stack
.push(RenderMethod::TextColored(text.to_string(), color));
let (w, h) = utils::str_size(text);
self.transform.width = self.transform.width.max(w);
self.transform.height = self.transform.height.max(h);
}
pub fn draw_rect(&mut self, width: u16, height: u16, color: u32) {
self.render_stack
.push(RenderMethod::Rectangle(width, height, color));
self.transform.width = self.transform.width.max(width);
self.transform.height = self.transform.height.max(height);
}
pub fn draw(&self) {
for m in &self.render_stack {
match m {
RenderMethod::Text(t) => {
utils::print(self.transform.x, self.transform.y, t);
}
RenderMethod::TextColored(t, c) => utils::print(
self.transform.x,
self.transform.y,
&(utils::hex_ansi(*c) + t),
),
RenderMethod::Rectangle(width, height, color) => {
utils::print_liner(
self.transform.x,
self.transform.y,
&hex_ansi_bg(*color),
&std::iter::repeat(" ".repeat(*width as usize))
.take(*height as usize)
.collect::<Vec<_>>()
.join("\n"),
);
}
}
}
}
pub fn clear(&mut self) {
self.render_stack.clear();
self.transform.width = 0;
self.transform.height = 0;
self.transform.x = 0;
self.transform.y = 0;
}
pub fn get_size(&self) -> (u16, u16) {
(self.transform.width, self.transform.height)
}
pub fn get_size_or(&self, width: u16, height: u16) -> (u16, u16) {
(
if self.transform.width == 0 {
width
} else {
self.transform.width
},
if self.transform.height == 0 {
height
} else {
self.transform.height
},
)
}
pub fn get_size_or_parent(&self) -> (u16, u16) {
(
if self.transform.width == 0 {
self.parent_width
} else {
self.transform.width
},
if self.transform.height == 0 {
self.parent_height
} else {
self.transform.height
},
)
}
pub fn get_parent_size(&self) -> (u16, u16) {
(self.parent_width, self.parent_height)
}
pub fn set_parent_size(&mut self, width: u16, height: u16) {
self.parent_width = width;
self.parent_height = height;
}
pub fn get_transform_mut(&mut self) -> &mut RawTransform {
&mut self.transform
}
pub fn get_transform(&self) -> &RawTransform {
&self.transform
}
}
impl Debug for RenderScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RenderScope")
}
}
+221 -46
View File
@@ -1,97 +1,272 @@
//! # State Management Module
//!
//! Provides React-like hooks for managing component state and side effects.
//! This module includes useState, useEffect, useMount, and state synchronization hooks.
use std::{ use std::{
any::Any,
fmt::{Debug, Display, Formatter, Result as FmtResult}, fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
#[derive(Debug, Clone)] use crate::component::context::Context;
pub struct State<T> {
inner: Arc<Mutex<Inner<T>>>,
}
/// Effect callback that can be triggered by state changes
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// State holder for reactive values
///
/// Similar to React's useState hook. Holds a value and tracks dependents
/// that need to be notified when the value changes.
#[derive(Debug)] #[derive(Debug)]
pub struct Inner<T> { pub struct State<T> {
value: T, /// The actual state value
dependencies: usize, value: Arc<Mutex<T>>,
changed: usize, /// Functions to call when state is updated
dependents: Arc<Mutex<Vec<HookEffect>>>,
} }
/// Guard for accessing and potentially modifying state
///
/// Dereferences to the state value. When dropped after modification,
/// automatically triggers all dependent effects.
pub struct Inner<'a, T> {
value: MutexGuard<'a, T>,
dependents: Arc<Mutex<Vec<HookEffect>>>,
updated: bool,
}
/// Mount lifecycle hook
///
/// Tracks whether a component has been mounted and executes
/// any pending mount effects.
#[derive(Debug, Clone)]
pub struct Mount(Arc<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
/// Creates a new state value
///
/// Returns a State that can be read and written from multiple threads.
pub fn use_state<T>(v: T) -> State<T> { pub fn use_state<T>(v: T) -> State<T> {
State { State {
inner: Arc::new(Mutex::new(Inner { value: Arc::new(Mutex::new(v)),
value: v, dependents: Arc::new(Mutex::new(Vec::new())),
dependencies: 0,
changed: 0,
})),
} }
} }
impl<T: Clone> State<T> { impl<T: Clone> State<T> {
/// Gets the cloned value, recommended for preventing deadlocks /// Gets a cloned copy of the state value
///
/// Recommended over `get()` to prevent deadlocks when cloning is acceptable.
/// "dl" stands for "deadlock-less".
pub fn get_dl(&self) -> T { pub fn get_dl(&self) -> T {
self.inner.lock().unwrap().value.clone() self.value.lock().unwrap().clone()
} }
} }
impl<T> State<T> { impl<T> State<T> {
/// Gets a lock on the state for read/write access. /// Acquires a lock on the state for read/write access
pub fn get(&self) -> MutexGuard<'_, Inner<T>> { ///
self.inner.lock().unwrap() /// Returns an Inner guard that implements Deref and DerefMut.
/// When dropped after modification, triggers dependent effects.
pub fn get(&self) -> Inner<'_, T> {
Inner {
value: self.value.lock().unwrap(),
dependents: self.dependents.clone(),
updated: false,
}
} }
/// Sets the value and marks it as changed. /// Sets the state value and triggers dependents
pub fn set(&self, v: T) { pub fn set(&self, v: T) {
let mut inner = self.inner.lock().unwrap(); *self.value.lock().unwrap() = v;
inner.value = v; self.update();
inner.changed = inner.dependencies;
} }
/// Marks the state as updated. /// Notifies all dependents of an update
pub fn update(&self) { pub fn update(&self) {
let mut inner = self.inner.lock().unwrap(); for d in self.dependents.lock().unwrap().iter() {
inner.changed = inner.dependencies; d.call();
} }
} }
impl<T: Debug + Send + Sync> DependencyHandler for State<T> { /// Clones the State handle (not the value)
fn check(&self) -> bool { pub fn clone(&self) -> Self {
let mut inner = self.inner.lock().unwrap(); Self {
let i = inner.changed > 0; dependents: self.dependents.clone(),
if i { value: self.value.clone(),
inner.changed -= 1;
} }
i
}
fn add(&self) {
let mut inner = self.inner.lock().unwrap();
inner.dependencies += 1;
} }
} }
impl<T: Display> Display for State<T> { impl<T: Display> Display for State<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let inner = self.inner.lock().unwrap(); write!(f, "{}", self.value.lock().unwrap())
write!(f, "{}", inner.value)
} }
} }
impl<T> Deref for Inner<T> { impl Debug for HookEffect {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "HookEffect")
}
}
impl HookEffect {
/// Creates a new effect from a function
pub fn new<F: Fn() + Send + Sync + 'static>(f: F) -> Self {
Self(Arc::new(Mutex::new(f)))
}
/// Executes the effect function
pub fn call(&self) {
(self.0.lock().unwrap())()
}
}
/// Inner implements Deref for read access and DerefMut for write access
/// On drop after mutation, automatically triggers dependent effects
impl<T> Drop for Inner<'_, T> {
fn drop(&mut self) {
if self.updated {
for d in self.dependents.lock().unwrap().iter() {
d.call();
}
}
}
}
impl<T> Deref for Inner<'_, T> {
type Target = T; type Target = T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.value &self.value
} }
} }
impl<T> DerefMut for Inner<T> { impl<T> DerefMut for Inner<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
self.changed = self.dependencies; self.updated = true;
&mut self.value &mut self.value
} }
} }
pub trait DependencyHandler: std::fmt::Debug + Send + Sync { /// Trait for values that can be tracked as dependencies in hooks
fn add(&self); pub trait HookDependency: Send + Sync {
fn check(&self) -> bool; /// Register an effect to be triggered on updates
fn on_update(&self, hook: HookEffect);
}
impl<T: Send + Sync> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook);
}
}
impl HookDependency for Mount {
fn on_update(&self, hook: HookEffect) {
if *self.0.lock().unwrap() {
hook.call();
} else {
self.1.lock().unwrap().push(hook);
}
}
}
impl Mount {
/// Mark the component as mounted and execute pending effects
pub fn mount(&self) {
*self.0.lock().unwrap() = true;
for hook_effect in self.1.lock().unwrap().iter() {
hook_effect.call();
}
self.1.lock().unwrap().clear();
}
}
/// Executes a function when dependencies change
///
/// Similar to React's useEffect. The provided function is executed
/// when any of the dependencies change.
pub fn use_effect<F: FnMut() + Send + Sync + 'static>(f: F, dependencies: &[&dyn HookDependency]) {
let f = Arc::new(Mutex::new(f));
let hook = HookEffect(Arc::new(Mutex::new({
let f = f.clone();
move || {
let f = f.clone();
std::thread::spawn(move || (f.lock().unwrap())());
}
})));
for d in dependencies {
d.on_update(hook.clone());
}
}
/// Creates a mount lifecycle hook
///
/// Returns a Mount that tracks component lifecycle and executes
/// effects after mounting.
pub fn use_mount() -> Mount {
Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new())))
}
/// Creates a manual mount lifecycle hook
///
/// Similar to use_mount but the component starts as unmounted.
/// Must call .mount() to trigger mounted effects.
pub fn use_mount_manual() -> Mount {
Mount(
Arc::new(Mutex::new(false)),
Arc::new(Mutex::new(Vec::new())),
)
}
/// Synchronizes state with events from the context
///
/// Creates state that is automatically updated when events are emitted
/// to the context. The decoder function converts events to state values.
pub fn use_sync_state<
T: Send + Sync + 'static,
E: Any + 'static,
D: Fn(&E) -> T + Send + Sync + 'static,
>(
cx: &Arc<Context>,
v: T,
decoder: D,
) -> State<T> {
let state = use_state(v);
cx.on_event({
let state = state.clone();
move |_, v: &E| state.set(decoder(v))
});
state
}
/// Synchronizes state changes back to the context as events
///
/// Creates an effect that emits an event whenever the state changes.
/// The encoder function converts state values to events.
pub fn use_sync_effect<
T: Send + Sync + 'static,
Ev: Send + Sync + 'static,
E: Fn(&State<T>) -> Ev + Send + Sync + 'static,
>(
cx: &Arc<Context>,
state: &State<T>,
encoder: E,
deps: &[&dyn HookDependency],
) {
use_effect(
{
let state = state.clone();
let cx = cx.clone();
move || {
let ev = encoder(&state);
cx.emit_event(ev);
}
},
deps,
);
} }
-153
View File
@@ -1,153 +0,0 @@
use crate::component;
#[derive(Debug, Clone)]
pub struct RawTransform {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone)]
pub enum Position {
Const(u16),
Center,
End,
}
#[derive(Debug, Clone)]
pub enum Dimension {
Full,
Content,
Const(u16),
}
component!(Transform {
pub x: Position,
pub y: Position,
pub mx: i32,
pub my: i32,
pub width: Dimension,
pub height: Dimension,
});
impl Transform {
pub fn new() -> Transform {
Transform {
x: Position::Const(0),
y: Position::Const(0),
mx: 0,
my: 0,
width: Dimension::Content,
height: Dimension::Content,
}
}
pub fn center() -> Transform {
Transform {
x: Position::Center,
y: Position::Center,
mx: 0,
my: 0,
width: Dimension::Content,
height: Dimension::Content,
}
}
pub fn bottom(mut self) -> Self {
self.y = Position::End;
self
}
pub fn right(mut self) -> Self {
self.x = Position::End;
self
}
pub fn margin(mut self, x: i32, y: i32) -> Self {
self.mx = x;
self.my = y;
self
}
pub fn dimensions(mut self, width: u16, height: u16) -> Self {
self.width = Dimension::Const(width);
self.height = Dimension::Const(height);
self
}
pub fn use_dimensions(&self, parent_width: u16, parent_height: u16, raw: &mut RawTransform) {
self.width.use_dimension(parent_width, &mut raw.width);
self.height.use_dimension(parent_height, &mut raw.height);
}
pub fn use_position(&self, parent_width: u16, parent_height: u16, raw: &mut RawTransform) {
self.x
.use_position(raw.width, parent_width, self.mx, &mut raw.x);
self.y
.use_position(raw.height, parent_height, self.my, &mut raw.y);
}
}
impl RawTransform {
pub fn new() -> RawTransform {
RawTransform {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Dimension {
pub fn use_dimension(&self, parent: u16, r: &mut u16) {
match self {
Self::Full => *r = parent,
Self::Content => {}
Self::Const(n) => *r = *n,
}
}
}
impl Position {
pub fn use_position(&self, size: u16, parent: u16, m: i32, r: &mut u16) {
let base = match self {
Self::Center => {
if parent < size {
return;
}
(parent - size) / 2
}
Self::Const(n) => *n,
Self::End => {
if parent < size {
return;
}
parent - size
}
};
let adjusted = if m >= 0 {
base.checked_add(m as u16)
} else {
base.checked_sub((-m) as u16)
};
if let Some(val) = adjusted {
*r = val;
}
}
}
impl From<u16> for Position {
fn from(value: u16) -> Self {
Self::Const(value)
}
}
impl From<u16> for Dimension {
fn from(value: u16) -> Self {
Self::Const(value)
}
}
-122
View File
@@ -1,122 +0,0 @@
//! The `utils` module provides utility functions for terminal manipulation
//! and string operations, designed to enhance terminal-based applications.
use std::io::{self, Write};
/// Clears the terminal screen and moves the cursor to the top-left corner.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::clear().unwrap();
/// ```
pub fn clear() -> io::Result<()> {
print!("\x1B[2J\x1B[H");
std::io::stdout().flush()
}
/// Hides the terminal cursor.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::hide_cursor().unwrap();
/// ```
pub fn hide_cursor() -> io::Result<()> {
print!("\x1b[?25l");
std::io::stdout().flush()
}
/// Shows the terminal cursor.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::show_cursor().unwrap();
/// ```
pub fn show_cursor() -> io::Result<()> {
print!("\x1B[?25h");
std::io::stdout().flush()
}
/// Flushes the terminal's stdout buffer.
///
/// # Returns
/// A `io::Result<()>` indicating whether the operation succeeded.
///
/// # Example
/// ```
/// utils::flush().unwrap();
/// ```
pub fn flush() -> io::Result<()> {
std::io::stdout().flush()
}
/// Calculates the width and height of a string when rendered in a terminal.
///
/// # Arguments
/// - `s`: The input string.
///
/// # Returns
/// A tuple `(u16, u16)` where:
/// - The first value is the maximum width of the string in characters.
/// - The second value is the height of the string in lines.
///
/// # Example
/// ```
/// let (width, height) = utils::str_size("Hello\nWorld!");
/// assert_eq!((5, 2), (width, height));
/// ```
pub fn str_size(s: &str) -> (u16, u16) {
let mut height = 1;
let mut max_width = 0;
let mut current_width = 0;
for b in s.bytes() {
if b == b'\n' {
height += 1;
max_width = max_width.max(current_width);
current_width = 0;
} else {
current_width += 1;
}
}
max_width = max_width.max(current_width);
(max_width, height)
}
pub fn hex_ansi(hex: u32) -> String {
let r = ((hex >> 16) & 0xFF) as u8;
let g = ((hex >> 8) & 0xFF) as u8;
let b = (hex & 0xFF) as u8;
format!("\x1b[38;2;{r};{g};{b}m")
}
pub fn hex_ansi_bg(hex: u32) -> String {
let r = ((hex >> 16) & 0xFF) as u8;
let g = ((hex >> 8) & 0xFF) as u8;
let b = (hex & 0xFF) as u8;
format!("\x1b[48;2;{r};{g};{b}m")
}
pub(crate) fn print(x: u16, y: u16, text: &str) {
for (i, line) in text.lines().enumerate() {
print!("\x1b[{};{}H{line}\x1b[0m", y + i as u16 + 1, x + 1);
flush().unwrap();
}
}
pub(crate) fn print_liner(x: u16, y: u16, liner: &str, text: &str) {
for (i, line) in text.lines().enumerate() {
print!("\x1b[{};{}H{liner}{line}\x1b[0m", y + i as u16 + 1, x + 1);
flush().unwrap();
}
}
-140
View File
@@ -1,140 +0,0 @@
use std::{
any::{Any, TypeId},
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::{render_scope::RenderScope, state::DependencyHandler};
pub type BoxedElement = Box<dyn Element + Send + Sync>;
pub type BoxedComponent = Box<dyn Component + Send + Sync>;
pub trait Element: Send + Sync {
#[allow(unused)]
fn render(&mut self, scope: &mut RenderScope) {}
#[allow(unused)]
fn after_render(&mut self, scope: &mut RenderScope) {}
#[allow(unused)]
fn draw_child(&self, element: &Arc<Widget>) {}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
pub trait Component: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
pub struct Widget(
pub Mutex<BoxedElement>,
Mutex<HashMap<TypeId, BoxedComponent>>,
Mutex<Box<dyn FnMut() -> WidgetLoad + Send + Sync>>,
Mutex<Vec<Box<dyn DependencyHandler>>>,
Mutex<Option<Box<dyn FnMut(WidgetLoad) -> WidgetLoad + Send + Sync>>>,
);
impl Widget {
pub fn new<F: FnMut() -> WidgetLoad + 'static + Send + Sync>(mut e: F) -> Self {
let wl = e();
Self(
Mutex::new(wl.0),
Mutex::new(wl.1),
Mutex::new(Box::new(e)),
Mutex::new(Vec::new()),
Mutex::new(None),
)
}
pub fn inject<F: FnMut(WidgetLoad) -> WidgetLoad + 'static + Send + Sync>(
self: &Arc<Self>,
f: F,
) {
{
*self.4.lock().unwrap() = Some(Box::new(f));
}
self.refresh();
}
pub fn refresh(self: &Arc<Self>) {
let w = (self.2.lock().unwrap())();
match &mut *self.4.lock().unwrap() {
Some(inject) => {
let w = inject(w);
*self.0.lock().unwrap() = w.0;
*self.1.lock().unwrap() = w.1;
}
None => {
*self.0.lock().unwrap() = w.0;
*self.1.lock().unwrap() = w.1;
}
}
}
pub fn auto_refresh(self: &Arc<Self>) {
for d in self.3.lock().unwrap().iter() {
if d.check() {
self.refresh();
}
}
}
pub fn dependency<D: DependencyHandler + 'static>(self: &Arc<Self>, d: D) -> &Arc<Self> {
d.add();
self.3.lock().unwrap().push(Box::new(d));
self
}
pub fn dependency_box(self: &Arc<Self>, d: Box<dyn DependencyHandler>) -> &Arc<Self> {
d.add();
self.3.lock().unwrap().push(d);
self
}
pub fn component<C: Component + 'static>(self: &Arc<Self>, c: C) -> &Arc<Self> {
self.1
.lock()
.unwrap()
.entry(c.type_id())
.or_insert_with(|| Box::new(c));
self
}
pub fn set_component<C: Component + 'static>(self: &Arc<Self>, c: C) -> &Arc<Self> {
self.1.lock().unwrap().insert(c.type_id(), Box::new(c));
self
}
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
self.1
.lock()
.unwrap()
.get(&TypeId::of::<C>())
.and_then(|c| c.as_any().downcast_ref::<C>())
.map(|c| c.clone())
}
}
pub struct WidgetLoad(BoxedElement, HashMap<TypeId, BoxedComponent>);
impl WidgetLoad {
pub fn new<E: Element + 'static>(e: E) -> Self {
Self(Box::new(e), HashMap::new())
}
pub fn component<C: Component + 'static>(mut self, c: C) -> Self {
self.1.entry(c.type_id()).or_insert_with(|| Box::new(c));
self
}
pub fn set_component<C: Component + 'static>(mut self, c: C) -> Self {
self.1.insert(c.type_id(), Box::new(c));
self
}
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> {
self.1
.get(&TypeId::of::<C>())
.and_then(|c| c.as_any().downcast_ref::<C>())
.map(|c| c.clone())
}
}