289 Commits
Author SHA1 Message Date
Klesti Selimaj b8db28629a Merge pull request #47 from osui-rs/frontend-enhancements
Frontend enhancements
2026-02-01 19:25:49 +01:00
selimaj-dev 6ac9f33a62 Benchmark 2026-02-01 19:13:15 +01:00
selimaj-dev 2606e9b4e1 Area merging 2026-02-01 19:03:12 +01:00
selimaj-dev 25bebcbb81 Flex components 2026-02-01 18:20:20 +01:00
selimaj-dev 76984187a0 View plugins for literals 2026-02-01 17:46:40 +01:00
selimaj-dev 44a4837647 view plugins Documentation 2026-02-01 17:36:48 +01:00
selimaj-dev e9541c0d21 proper auto sizing 2026-02-01 17:25:49 +01:00
selimaj-dev c4addd7f01 View plugins 2026-02-01 17:18:59 +01:00
selimaj-dev 2ca419bfd4 Fixed newline drawing in engine 2026-02-01 16:37:07 +01:00
selimaj-dev b0eaa76398 View plugins 2026-02-01 16:34:27 +01:00
selimaj-dev d40a032bc1 Improved state structure 2026-02-01 13:40:22 +01:00
selimaj-dev 8c4576396b Improved state structure 2026-02-01 13:39:37 +01:00
selimaj-dev 039e751dd1 Removed dupes of deps in {} 2026-02-01 11:01:20 +01:00
selimaj-dev c3519fa6ed Improved Card by dynamicifying the bars 2026-02-01 10:31:37 +01:00
selimaj-dev 5e4b78f353 Removed excess clone in Card component 2026-02-01 10:23:34 +01:00
selimaj-dev b141e134d4 Direct text dependencies 2026-02-01 10:21:28 +01:00
selimaj-dev b23c8c68dc Text dependencies 2026-02-01 10:11:41 +01:00
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
selimaj-dev 5d1361621d version .9alpha2 2025-07-29 20:55:06 +02:00
selimaj-dev 5ca7221afa outlines 2025-07-29 20:54:12 +02:00
selimaj-dev 71660418a1 prelude and transform macro 2025-07-29 19:42:56 +02:00
selimaj-dev 26eb1679f6 size full and content 2025-07-29 18:16:37 +02:00
selimaj-dev db66803347 state management as a child 2025-07-29 17:51:03 +02:00
selimaj-dev de9039dbaf refactored flex 2025-07-29 17:44:10 +02:00
selimaj-dev e2d7c1ef91 refactored div 2025-07-29 17:08:25 +02:00
selimaj-dev 3a7d69cb04 removed styling 2025-07-29 16:25:52 +02:00
selimaj-dev a75c02fa90 fixed nested issue 2025-07-29 01:51:07 +02:00
selimaj-dev 250dde8f9e rename grid to flex 2025-07-28 19:44:16 +02:00
selimaj-dev e3bbda6b59 expandable rsx 2025-07-28 19:43:58 +02:00
selimaj-dev b0b1835cc1 div example 2025-07-28 19:27:15 +02:00
selimaj-dev a59a8eeb73 Widget injection 2025-07-28 19:24:50 +02:00
selimaj-dev a8e832892b rsx child 2025-07-28 18:47:59 +02:00
selimaj-dev 26c112bdb2 rsx component 2025-07-28 15:32:11 +02:00
selimaj-dev a79617e219 changed main code 2025-07-28 15:28:44 +02:00
selimaj-dev 10fae90ecc small fix with rsx 2025-07-28 15:23:12 +02:00
selimaj-dev 5c2c5ba46b Improved frontend 2025-07-28 15:22:17 +02:00
selimaj-dev 211b5aa95c Moved state 2025-07-28 15:02:35 +02:00
selimaj-dev 02bdca151b Dynamic Widget system 2025-07-28 15:02:22 +02:00
selimaj-dev 00f7493741 Rsx 2025-07-28 00:03:51 +02:00
selimaj-dev d9c23f1f09 Rsx 2025-07-28 00:01:12 +02:00
selimaj-dev cba301db74 State management 2025-07-27 19:53:45 +02:00
selimaj-dev 327f14d9ed semi working state management 2025-07-27 02:35:36 +02:00
selimaj-dev 0878cad01c Simple state management 2025-07-26 00:26:30 +02:00
selimaj-dev 3d995671cd resource management 2025-07-07 10:05:23 -05:00
selimaj-dev 5fabd1a8ab resources (not fully working yet) 2025-07-06 02:12:10 -05:00
selimaj-dev 4dfbee5984 position fix 2025-07-05 10:06:48 -05:00
selimaj-dev 2f328f6762 working grid dimensions 2025-07-05 09:13:05 -05:00
selimaj-dev 8ae6fb49db fully working h and v grid 2025-07-05 05:21:16 -05:00
selimaj-dev 6255b9c4f4 vgrid 2025-07-05 04:59:40 -05:00
selimaj-dev ec963707a1 working divs 2025-07-05 04:40:05 -05:00
selimaj-dev cec25c02d5 better handlers 2025-07-05 06:48:21 +02:00
selimaj-dev 661756eccd handlers and ids 2025-07-05 06:28:50 +02:00
selimaj-dev 921bfa27c1 improved velocity algorithm 2025-06-30 08:31:28 -05:00
selimaj-dev bbaa9d09e1 multithreading 2025-06-29 22:56:53 +02:00
selimaj-dev 85bfa0f45f working extension system 2025-06-29 11:49:11 -05:00
selimaj-dev af719d83db redesign 2025-06-29 11:20:36 -05:00
selimaj-dev cff2e315ca alpha 1 2025-06-29 01:51:57 +02:00
selimaj-dev eb64a77aac state fix 2025-06-28 16:00:16 -05:00
selimaj-dev bb0ef3adc4 improved state management 2025-06-28 11:30:09 -05:00
selimaj-dev fd73514a7b recoursive event lock fix 2025-06-28 08:13:12 -05:00
selimaj-dev f43be496c1 better structure 2025-06-28 15:02:45 +02:00
selimaj-dev f401d4a995 better structure 2025-06-28 03:51:13 +02:00
selimaj-dev 551a062021 better state management 2025-06-28 01:57:36 +02:00
selimaj-dev 627ab80338 state management 2025-06-27 15:51:22 -05:00
selimaj-dev 3adfff41fb testing state management 2025-06-27 15:32:55 -05:00
selimaj-dev 6c8ff5f3da margin 2025-06-27 15:06:45 -05:00
selimaj-dev 9715630d9f few changes 2025-06-27 15:02:13 -05:00
selimaj-dev 3cacd42421 event handler on events 2025-06-27 12:22:59 -05:00
selimaj-dev 15b61d57c8 slightly improved structure and added a few more elements 2025-06-27 12:10:25 -05:00
selimaj-dev dfb17ab48e working center 2025-06-26 02:42:27 -05:00
selimaj-dev 82b847314e transform and components 2025-06-26 09:24:35 +02:00
selimaj-dev f363bbdae1 slight re-structure 2025-06-26 07:13:41 +02:00
selimaj-dev 8274afff04 finally working event system 2025-06-25 14:25:26 -05:00
selimaj-dev 7693711aa9 test 2025-06-25 17:18:23 +02:00
selimaj-dev 08f1d3364b testing for event handling 2025-06-24 15:25:25 -05:00
selimaj-dev 6c0dde8e85 test 2025-06-24 17:21:12 +02:00
selimaj-dev f72959bcee test 2025-06-24 03:16:24 +02:00
selimaj-dev 52f4dcfb2f mouse events 2025-01-21 21:06:10 +01:00
selimaj-dev 09d1ee21bf done 2025-01-21 12:14:02 +01:00
selimaj-dev aa55cc059a documented the code 2025-01-20 00:51:03 +01:00
selimaj-dev fda5387ccc few changes 2025-01-19 23:24:55 +01:00
selimaj-dev 5a21f9f740 some small changes 2025-01-19 13:14:30 +01:00
selimaj-dev 49ce197f78 if tree 2025-01-18 23:55:46 +01:00
selimaj-dev fb82c611df small changes 2025-01-18 23:39:21 +01:00
selimaj-dev ab5554abce expanded components 2025-01-18 18:13:45 +01:00
selimaj-dev 9ecf8de501 added positioning in rsx 2025-01-18 15:04:09 +01:00
selimaj-dev bde4438663 few changes 2025-01-17 22:13:42 +01:00
selimaj-dev a2ec2f9d41 few changes 2025-01-17 13:18:39 +01:00
selimaj-dev e2e1a466f4 state management 2025-01-17 02:28:54 +01:00
selimaj-dev 85dd5a6f66 few changes 2025-01-17 01:01:27 +01:00
selimaj-dev a6472f8e2e few changes 2025-01-17 01:01:14 +01:00
selimaj-dev eeefd1f002 rewrite 2025-01-16 23:47:34 +01:00
selimaj-dev 6878777140 rewrite 2025-01-15 09:25:58 -06:00
38 changed files with 2742 additions and 1823 deletions
+5
View File
@@ -0,0 +1,5 @@
debug
target
github
Cargo.lock
.DS_Store
+11 -4
View File
@@ -1,12 +1,15 @@
[package] [package]
name = "osui" name = "osui"
version = "0.0.8-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"
repository = "https://github.com/osui-rs/osui" repository = "https://github.com/osui-rs/osui"
homepage = "https://osui.netlify.app" homepage = "https://osui.netlify.app"
documentation = "https://osui.netlify.app/docs/" documentation = "https://osui.netlify.app/docs/"
[features]
rsx = []
[lib] [lib]
name = "osui" name = "osui"
@@ -20,9 +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"
dyn-clone = "1.0.17" figlet-rs = "0.1.5"
osui-element = "0.1.23" osui-macros = { version = "0.1.0", path = "macros" }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
@@ -40,3 +44,6 @@ overflow-checks = false
lto = true lto = true
panic = 'abort' panic = 'abort'
codegen-units = 1 codegen-units = 1
[workspace]
members = [".", "macros"]
+18 -1
View File
@@ -17,9 +17,26 @@
</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
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)
}
```
## [Documentation](https://osui.netlify.app/docs) ## [Documentation](https://osui.netlify.app/docs)
## Features ## Features
+227
View File
@@ -0,0 +1,227 @@
Iterx72,Nestingx72,Time µs
14,14,7668
0,4,714
5,1,1550
3,0,617
9,6,2399
0,3,882
11,10,2691
4,10,1343
5,5,1562
6,6,1749
11,3,2719
9,14,2315
1,13,770
14,8,3290
6,14,1767
6,11,1857
10,11,2530
7,11,1936
13,5,3130
3,13,1161
0,2,928
6,3,1751
11,11,2727
11,6,2860
3,12,1170
13,14,3090
3,2,1140
12,4,2952
9,11,4884
10,2,2726
1,3,756
3,6,1151
6,5,1731
9,1,2348
11,4,2688
2,13,944
13,3,3125
12,0,643
1,4,721
6,1,1757
1,14,730
10,7,2505
7,9,2002
13,6,3132
4,6,1354
4,7,1311
0,14,520
4,8,1372
14,7,3304
7,0,620
0,0,605
11,9,2748
4,13,1389
12,10,2989
8,1,2211
9,9,2323
4,11,1404
5,9,1569
3,11,1129
5,7,1567
10,14,2543
5,0,612
10,0,636
6,4,1745
5,4,1606
1,2,707
11,12,2727
13,0,605
8,2,2127
10,4,2512
4,4,1340
6,8,1973
7,8,2185
3,10,1175
9,2,2300
1,6,743
10,1,2547
1,5,715
3,1,1209
7,4,2217
4,5,1346
11,1,2719
7,10,1972
9,3,2351
14,13,3301
14,3,3315
2,12,939
13,8,3098
9,13,2339
11,7,2749
6,7,1742
9,8,2348
8,11,2171
7,6,2370
12,8,2988
4,2,1349
2,8,955
8,4,2165
2,5,938
1,0,765
14,1,3275
14,11,3349
2,11,1024
14,2,3423
12,12,2844
8,12,2219
14,5,3308
3,14,1136
1,9,710
6,13,1831
8,13,2296
11,5,2729
4,1,1364
14,10,3324
12,5,2925
8,5,2121
10,3,2519
1,7,698
2,4,959
6,9,1973
7,5,2240
2,7,936
0,6,753
5,10,1557
2,3,948
2,14,931
8,6,2132
2,10,936
4,0,618
13,4,3136
5,6,1530
5,8,1546
7,14,1920
8,10,2110
12,9,3021
1,11,738
13,11,3121
1,1,730
0,1,981
3,8,1271
10,8,2554
14,14,3267
0,9,600
4,9,1328
9,12,3707
6,12,1800
0,5,684
7,2,2180
9,10,7168
8,14,2163
7,7,3732
8,0,633
5,14,1645
8,8,2129
12,7,2898
0,10,573
7,3,2255
14,9,3272
2,9,924
9,4,2331
10,5,2513
10,13,2514
11,8,2733
14,12,3326
0,8,618
6,0,609
8,9,2157
12,1,3013
12,11,2916
12,3,2959
9,0,623
12,13,3064
0,13,536
5,13,1558
6,10,1835
11,13,2784
13,9,3071
0,7,599
2,0,617
9,5,2303
8,3,2152
12,2,2994
0,12,542
7,13,1923
4,3,1350
13,7,3177
10,10,2533
6,2,1748
3,7,1209
13,2,3097
11,2,2731
5,2,1562
14,4,3273
7,1,2088
9,7,2333
5,12,1556
3,5,1177
12,14,2888
3,4,1153
1,10,710
11,0,614
10,12,2505
11,14,2827
1,8,710
14,6,3282
7,12,1938
2,2,943
10,9,2578
0,11,537
2,1,926
4,14,1367
14,0,614
1,12,716
2,6,935
13,10,3105
10,6,2524
4,12,1393
8,7,2156
12,6,2897
5,11,1528
13,13,3129
5,3,1536
3,3,1179
3,9,1173
13,12,3094
13,1,3097
1 Iterx72 Nestingx72 Time µs
2 14 14 7668
3 0 4 714
4 5 1 1550
5 3 0 617
6 9 6 2399
7 0 3 882
8 11 10 2691
9 4 10 1343
10 5 5 1562
11 6 6 1749
12 11 3 2719
13 9 14 2315
14 1 13 770
15 14 8 3290
16 6 14 1767
17 6 11 1857
18 10 11 2530
19 7 11 1936
20 13 5 3130
21 3 13 1161
22 0 2 928
23 6 3 1751
24 11 11 2727
25 11 6 2860
26 3 12 1170
27 13 14 3090
28 3 2 1140
29 12 4 2952
30 9 11 4884
31 10 2 2726
32 1 3 756
33 3 6 1151
34 6 5 1731
35 9 1 2348
36 11 4 2688
37 2 13 944
38 13 3 3125
39 12 0 643
40 1 4 721
41 6 1 1757
42 1 14 730
43 10 7 2505
44 7 9 2002
45 13 6 3132
46 4 6 1354
47 4 7 1311
48 0 14 520
49 4 8 1372
50 14 7 3304
51 7 0 620
52 0 0 605
53 11 9 2748
54 4 13 1389
55 12 10 2989
56 8 1 2211
57 9 9 2323
58 4 11 1404
59 5 9 1569
60 3 11 1129
61 5 7 1567
62 10 14 2543
63 5 0 612
64 10 0 636
65 6 4 1745
66 5 4 1606
67 1 2 707
68 11 12 2727
69 13 0 605
70 8 2 2127
71 10 4 2512
72 4 4 1340
73 6 8 1973
74 7 8 2185
75 3 10 1175
76 9 2 2300
77 1 6 743
78 10 1 2547
79 1 5 715
80 3 1 1209
81 7 4 2217
82 4 5 1346
83 11 1 2719
84 7 10 1972
85 9 3 2351
86 14 13 3301
87 14 3 3315
88 2 12 939
89 13 8 3098
90 9 13 2339
91 11 7 2749
92 6 7 1742
93 9 8 2348
94 8 11 2171
95 7 6 2370
96 12 8 2988
97 4 2 1349
98 2 8 955
99 8 4 2165
100 2 5 938
101 1 0 765
102 14 1 3275
103 14 11 3349
104 2 11 1024
105 14 2 3423
106 12 12 2844
107 8 12 2219
108 14 5 3308
109 3 14 1136
110 1 9 710
111 6 13 1831
112 8 13 2296
113 11 5 2729
114 4 1 1364
115 14 10 3324
116 12 5 2925
117 8 5 2121
118 10 3 2519
119 1 7 698
120 2 4 959
121 6 9 1973
122 7 5 2240
123 2 7 936
124 0 6 753
125 5 10 1557
126 2 3 948
127 2 14 931
128 8 6 2132
129 2 10 936
130 4 0 618
131 13 4 3136
132 5 6 1530
133 5 8 1546
134 7 14 1920
135 8 10 2110
136 12 9 3021
137 1 11 738
138 13 11 3121
139 1 1 730
140 0 1 981
141 3 8 1271
142 10 8 2554
143 14 14 3267
144 0 9 600
145 4 9 1328
146 9 12 3707
147 6 12 1800
148 0 5 684
149 7 2 2180
150 9 10 7168
151 8 14 2163
152 7 7 3732
153 8 0 633
154 5 14 1645
155 8 8 2129
156 12 7 2898
157 0 10 573
158 7 3 2255
159 14 9 3272
160 2 9 924
161 9 4 2331
162 10 5 2513
163 10 13 2514
164 11 8 2733
165 14 12 3326
166 0 8 618
167 6 0 609
168 8 9 2157
169 12 1 3013
170 12 11 2916
171 12 3 2959
172 9 0 623
173 12 13 3064
174 0 13 536
175 5 13 1558
176 6 10 1835
177 11 13 2784
178 13 9 3071
179 0 7 599
180 2 0 617
181 9 5 2303
182 8 3 2152
183 12 2 2994
184 0 12 542
185 7 13 1923
186 4 3 1350
187 13 7 3177
188 10 10 2533
189 6 2 1748
190 3 7 1209
191 13 2 3097
192 11 2 2731
193 5 2 1562
194 14 4 3273
195 7 1 2088
196 9 7 2333
197 5 12 1556
198 3 5 1177
199 12 14 2888
200 3 4 1153
201 1 10 710
202 11 0 614
203 10 12 2505
204 11 14 2827
205 1 8 710
206 14 6 3282
207 7 12 1938
208 2 2 943
209 10 9 2578
210 0 11 537
211 2 1 926
212 4 14 1367
213 14 0 614
214 1 12 716
215 2 6 935
216 13 10 3105
217 10 6 2524
218 4 12 1393
219 8 7 2156
220 12 6 2897
221 5 11 1528
222 13 13 3129
223 5 3 1536
224 3 3 1179
225 3 9 1173
226 13 12 3094
227 13 1 3097
+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)
}
}
+31
View File
@@ -0,0 +1,31 @@
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! {
// redraw is important because the effects won't be applied
impl size_auto, center, redraw
FlexRow {
impl size_auto, redraw
Card { content: "One".to_string() }
impl size_auto, redraw
Card { content: "Two".to_string() }
}
}
.view(&cx)
}
#[component]
fn Card(cx: &Arc<Context>, content: String) -> View {
let bar = "-".repeat(content.len());
rsx! {
"{bar}\n{content}\n{bar}"
}
.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"
+237
View File
@@ -0,0 +1,237 @@
//! # 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().filter(|d| d.is_dep).map(|d| {
let ident = &d.ident;
quote! {
std::sync::Arc::new(#ident) as std::sync::Arc<dyn HookDependency>
}
});
quote! {
vec![ #(#deps),* ]
}
}
/// Emits a Vec of plugins
fn emit_plugins(deps: &[ViewPlugin]) -> TokenStream {
deps.iter()
.map(|d| {
let path = &d.path;
if let Some(args) = &d.args {
quote!( #path(ctx, &view #(,#args)*); )
} else {
quote!( #path(ctx, &view); )
}
})
.collect()
}
/// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text { deps, .. } => {
let emit = emit_node(node);
let deps_emit = emit_deps(deps);
quote! {
#deps_emit
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,
deps,
plugins,
} => {
let deps_emit = emit_deps(deps);
if plugins.len() == 0 {
quote! {
scope.view(Arc::new({
#deps_emit
move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}));
}
} else {
let plugins_emit = emit_plugins(plugins);
quote! {
scope.view_wrapped(Arc::new({
#deps_emit
move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}), std::sync::Arc::new(|ctx, view| {
let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height);
ctx.draw_view(area, view.clone());
#plugins_emit
}));
}
}
}
RsxNode::Component {
plugins,
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,)*
}
}
};
if plugins.len() == 0 {
quote! {
scope.child(#component_expr, None);
}
} else {
let plugins_emit = emit_plugins(plugins);
quote! {
scope.child(#component_expr, Some(std::sync::Arc::new(|ctx, view| {
let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height);
ctx.draw_view(area, view.clone());
#plugins_emit
})));
}
}
}
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()
}
+355
View File
@@ -0,0 +1,355 @@
//! # RSX Parser
//!
//! Parses RSX syntax into an AST that can be emitted as Rust code.
use std::collections::HashSet;
use proc_macro2::Span;
use syn::parse::discouraged::Speculative;
use syn::punctuated::Punctuated;
use syn::{
Expr, Ident, LitStr, Pat, Path, Result, Token,
parse::{Parse, ParseStream},
token::Brace,
};
use syn::{braced, parenthesized};
/// 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 {
plugins: Vec<ViewPlugin>,
text: LitStr,
deps: Vec<Dep>,
},
/// Expression node: `{expr}`
Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }`
Component {
/// View plugins
plugins: Vec<ViewPlugin>,
/// 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>,
pub is_dep: bool,
}
pub struct ViewPlugin {
pub path: Path,
pub args: Option<Vec<Expr>>,
}
fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
let mut deps = Vec::new();
if input.peek(Token![%]) {
input.parse::<Token![%]>()?;
loop {
let is_ref = if input.peek(Token![ref]) {
input.parse::<Token![ref]>()?;
true
} else {
false
};
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,
is_dep: !is_ref,
});
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(deps)
}
fn parse_plugins(input: ParseStream) -> Result<Vec<ViewPlugin>> {
let mut plugins = Vec::new();
if input.peek(Token![impl]) {
input.parse::<Token![impl]>()?;
loop {
let path: Path = input.parse()?;
let args = if input.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let exprs = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
Some(exprs.into_iter().collect())
} else {
None
};
plugins.push(ViewPlugin { path, args });
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}
}
Ok(plugins)
}
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,
});
}
let plugins = parse_plugins(input)?;
// "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) {
let text = input.parse()?;
let mut deps = deps;
deps.append(
&mut extract_vars_from_lit(&text)
.into_iter()
.map(|ident| Dep {
ident,
pat: None,
is_dep: false,
})
.collect(),
);
return Ok(RsxNode::Text {
text,
deps,
plugins,
});
}
parse_component_invocation(input, plugins)
}
}
/// Parses Path or Path { ... } into RsxNode::Component.
fn parse_component_invocation(input: ParseStream, plugins: Vec<ViewPlugin>) -> Result<RsxNode> {
let path: Path = input.parse()?;
if !input.peek(Brace) {
return Ok(RsxNode::Component {
plugins,
path,
props: Vec::new(),
children: Vec::new(),
});
}
let content;
braced!(content in input);
let (props, children) = parse_props_and_children(&content)?;
Ok(RsxNode::Component {
plugins,
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)
}
fn extract_vars_from_lit(lit: &LitStr) -> HashSet<Ident> {
let s = lit.value();
let mut vars = HashSet::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
// skip escaped {{
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut name = String::new();
while let Some(&ch) = chars.peek() {
chars.next();
if ch == '}' {
break;
}
name.push(ch);
}
if !name.is_empty() {
vars.insert(Ident::new(
&name.split_once(':').unwrap_or((&name, &name)).0,
Span::call_site(),
));
}
}
}
vars
}
+51
View File
@@ -0,0 +1,51 @@
use crate::component_prelude::*;
#[component]
pub fn FlexRow(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.y += ctx2.allocated.height;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
#[component]
pub fn FlexColumn(cx: &Arc<Context>, children: Rsx) -> View {
children.generate_children(&cx);
Arc::new({
let cx = cx.clone();
move |ctx| {
for (child, view_wrapper) in cx.get_children() {
let view = child.get_view();
if let Some(view_wrapper) = view_wrapper {
let mut ctx2 = ctx.clone();
view_wrapper(&mut ctx2, view);
ctx.drawing.append(&mut ctx2.drawing);
ctx.area.x += ctx2.allocated.width;
} else {
ctx.draw_view(ctx.area.clone(), view);
}
}
}
})
}
+3
View File
@@ -0,0 +1,3 @@
mod flex;
pub use flex::*;
+259
View File
@@ -0,0 +1,259 @@
//! # 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},
hooks::{use_effect, HookDependency},
render::DrawContext,
View, ViewWrapper,
};
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);
}
/// Gets the children
pub fn get_children(self: &Arc<Self>) -> Vec<(Arc<Context>, Option<ViewWrapper>)> {
self.scopes
.lock()
.unwrap()
.iter()
.flat_map(|s| s.children.lock().unwrap().clone())
.collect::<Vec<_>>()
}
/// 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)
}
}
+45
View File
@@ -0,0 +1,45 @@
//! # 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 components;
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)
}
}
+65
View File
@@ -0,0 +1,65 @@
//! # 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));
}
/// Adds a view directly to this scope
pub fn view_wrapped(self: &Arc<Self>, view: View, view_wrapper: ViewWrapper) {
let ctx = Context::new(view, self.executor.clone());
ctx.refresh();
self.children.lock().unwrap().push((ctx, Some(view_wrapper)));
}
}
-206
View File
@@ -1,206 +0,0 @@
#[macro_export]
macro_rules! __css {
($_style:expr,)=>{};
// percentage (n%)
(
$_style:expr, $name:ident: $value:literal% $(, $($rest:tt)*)?
) => {{
$_style.$name = (true, Number::Pe($value));
$crate::__css!($_style, $($($rest)*)?);
}};
// Number
(
$_style:expr, $name:ident: $value:literal px $(, $($rest:tt)*)?
) => {{
$_style.$name = (true, Number::Px($value));
$crate::__css!($_style, $($($rest)*)?);
}};
// Vector
(
$_style:expr, $name:ident: [$($inner:tt),*] $(, $($rest:tt)*)?
) => {{
$_style.$name = (true, vec![$($inner),*]);
$crate::__css!($_style, $($($rest)*)?);
}};
// Normal
(
$_style:expr, $name:ident: $value:expr $(, $($rest:tt)*)?
) => {{
$_style.$name = (true, $value);
$crate::__css!($_style, $($($rest)*)?);
}};
}
#[macro_export]
macro_rules! _css {
($_style:expr,) => {};
(
$hm:expr, $($($name:ident)-+),* { $($inner:tt)* } $($rest:tt)*
) => {{
let mut _style = $crate::ui::StyleElement::default();
$crate::__css!(_style, $($inner)*);
$(
$hm.insert(
$crate::ui::StyleName::Class(String::from(stringify!($($name)-+).replace(' ', ""))),
_style.clone(),
);
)*
$crate::_css!($hm, $($rest)*);
}};
(
$hm:expr, $($($name:ident)-+),*: $state:literal { $($inner:tt)* } $($rest:tt)*
) => {{
let mut _style = $crate::ui::StyleElement::default();
$crate::__css!(_style, $($inner)*);
$(
$hm.insert(
$crate::ui::StyleName::ClassState(String::from(stringify!($($name)-+).replace(' ', "")), String::from($state)),
_style.clone(),
);
)*
$crate::_css!($hm, $($rest)*);
}};
(
$hm:expr, $($name:literal),+ { $($inner:tt)* } $($rest:tt)*
) => {{
let mut _style = $crate::ui::StyleElement::default();
$crate::__css!(_style, $($inner)*);
$(
$hm.insert(
$crate::ui::StyleName::Class(String::from($name)),
_style,
);
)+
$crate::_css!($hm, $($rest)*);
}};
(
$hm:expr, $($name:literal),+: $state:literal { $($inner:tt)* } $($rest:tt)*
) => {{
let mut _style = $crate::ui::StyleElement::default();
$crate::__css!(_style, $($inner)*);
$(
$hm.insert(
$crate::ui::StyleName::ClassState(String::from($name), String::from($state)),
_style,
);
)+
$crate::_css!($hm, $($rest)*);
}};
}
/// Creates a styling map `Css`
///
/// # Example
/// ```
/// css! {
/// "btn" {
/// color: Red,
/// }
/// "btn": "clicked" {
/// color: Blue,
/// }
/// }
/// ```
///
/// # Returns
/// `Css`
#[macro_export]
macro_rules! css {
(
$($inner:tt)*
) => {{
#[allow(unused_mut)]
let mut hm: $crate::ui::Css = std::collections::HashMap::new();
$crate::_css!(hm, $($inner)*);
hm
}};
}
#[macro_export]
macro_rules! style {
(
$key:ident: $value:literal% $(, $($rest:tt)*)?
) => {{
let mut _style = $crate::ui::Style::default();
$crate::__css!(_style.0, $key: $crate::ui::Number::Pe($value));
$crate::style!{ _style; $($($rest)*)? }
}};
(
$key:ident: $value:literal px $(, $($rest:tt)*)?
) => {{
let mut _style = $crate::ui::Style::default();
$crate::__css!(_style.0, $key: $crate::ui::Number::Px($value));
$crate::style!{ _style; $($($rest)*)? }
}};
(
$style:expr; $key:ident: $value:literal% $(, $($rest:tt)*)?
) => {{
$crate::__css!($style.0, $key: $crate::ui::Number::Pe($value));
$crate::style!{ $style; $($($rest)*)? }
}};
(
$style:expr; $key:ident: $value:literal px $(, $($rest:tt)*)?
) => {{
$crate::__css!($style.0, $key: $crate::ui::Number::Px($value));
$crate::style!{ $style; $($($rest)*)? }
}};
(
$key:ident: $value:expr $(, $($rest:tt)*)?
) => {{
let mut _style = $crate::ui::Style::default();
$crate::__css!(_style.0, $key: $value);
$crate::style!{ _style; $($($rest)*)? }
}};
(
$style:expr; $key:ident: $value:expr $(, $($rest:tt)*)?
) => {{
let mut style_elem = $crate::ui::StyleElement::default();
$crate::__css!($style.0, $key: $value);
$crate::style!{ $style; $($($rest)*)? }
}};
(
($key:ident): $value:expr $(, $($rest:tt)*)?
) => {{
let mut _style = $crate::ui::Style::default();
$crate::__css!(_style.0, ($key): $value);
$crate::style!{ _style; $($($rest)*)? }
}};
(
$style:expr; $key:literal: $value:expr $(, $($rest:tt)*)?
) => {{
let mut style_elem = $crate::ui::StyleElement::default();
$crate::__css!($style.0, $key: $value);
$crate::style!{ $style; $($($rest)*)? }
}};
(
$kind:ident {$($inner:tt)*} $(, $($rest:tt)*)?
) => {{
let mut _style = $crate::ui::Style::default();
let mut style_elem = $crate::ui::StyleElement::default();
$crate::__css!(style_elem, $($inner)*);
_style.1.insert(stringify!($kind).to_string(), style_elem);
$crate::style!{ _style; $($($rest)*)? }
}};
(
$style:expr; $kind:ident {$($inner:tt)*} $(, $($rest:tt)*)?
) => {{
let mut style_elem = $crate::ui::StyleElement::default();
$crate::__css!(style_elem, $($inner)*);
$style.1.insert(stringify!($kind).to_string(), style_elem);
$crate::style!{ $style; $($($rest)*)? }
}};
(
$style:expr;
) => {{
$style
}};
() => {{
$crate::ui::Style::default()
}};
}
+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
}
}
+155
View File
@@ -0,0 +1,155 @@
//! # 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);
for (i, line) in text.lines().enumerate() {
execute!(stdout(), MoveTo(x, y + (i as u16))).unwrap();
print!("{line}");
}
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<()>;
}
-132
View File
@@ -1,132 +0,0 @@
pub mod counter_example {
use crate::prelude::*;
#[component]
pub fn App() -> Element {
let count = use_state(0);
rsx! {
@SetStyle(styling())
button {
class: "btn",
on_click: fn(_, _, _) @count {
count += 1;
},
"The current count is: {count}"
}
}
}
pub fn styling() -> Css {
css! {
btn {
outline: true,
outline_color: Red
}
}
}
}
pub mod todo_example {
use crate::prelude::*;
#[component]
pub fn App() -> Element {
let todo = vec!["Todo", "Hovering", "Completed", "Foo", "Bar"];
rsx! {
@SetStyle(styling())
@SetChild(2)
text { class: "title", static "Todo list app made with OSUI" }
text { class: "title", "{}", "-".repeat(crossterm::terminal::size().unwrap().0 as usize) }
for (t in todo) {
ersx!(button { class: "todo", on_click: fn(btn: &mut Button, _, _) {
btn.class = if btn.class == "todo" {
"complete"
} else {
"todo"
};
}, "{t}" } )
}
}
}
pub fn styling() -> Css {
css! {
todo {
x: 0 px,
y: Auto,
color: Red,
}
todo: "hover" {
color: Blue,
}
complete {
x: 0 px,
y: Auto,
color: Green,
}
complete: "hover" {
color: Cyan
}
title {
x: 0 px,
y: Auto,
}
title: "hover" {
color: Blue
}
}
}
}
pub mod login_example {
use crate::prelude::*;
#[component]
pub fn App() -> Element {
rsx! {
@SetStyle(styling())
input { class: "input", id: "usr", on_click: fn(_, _, document) {
let root = document.get_root::<Div>();
root.children.set_index(1);
}, "username: " }
input { class: "input", id: "psw", on_click: fn(_, _, document) {
let root = document.get_root::<Div>();
root.children.set_index(2);
}, "password: " }
button { class: "input", on_click: fn(_, _, document) {
if let Some(usr) = document.get_element_by_id::<Input>("usr") {
if let Some(psw) = document.get_element_by_id::<Input>("psw") {
document.draw(rsx! {
text { static "Username: {}\nPassword: {}", usr.text, psw.text }
});
}
}
}, "Submit" }
}
}
pub fn styling() -> Css {
css! {
input {
width: 50 px,
outline: true,
y: Auto,
x: 0 px,
caret: String::new(),
}
input: "hover" {
outline_color: Red,
caret: String::from("█"),
}
}
}
}
+122
View File
@@ -0,0 +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 crate::component::{context::Context, scope::Scope};
use crate::{render::Point, hooks::HookDependency, View};
/// Trait for converting values to RSX
pub trait ToRsx {
/// Convert to RSX representation
fn to_rsx(&self) -> Rsx;
}
/// 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 {
/// Creates a new empty RSX
pub fn new() -> Self {
Self(Vec::new())
}
/// Adds a static scope to this RSX
///
/// The provided function is executed once during rendering
/// and will not be re-executed on dependency changes.
pub fn static_scope<F: Fn(&Arc<Scope>) + Send + Sync + 'static>(&mut self, scope: F) {
self.0.push(RsxScope::Static(Arc::new(scope)));
}
/// 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,
drawer: F,
dependencies: Vec<Arc<dyn HookDependency>>,
) {
self.0
.push(RsxScope::Dynamic(Arc::new(drawer), dependencies));
}
/// Adds a child RSX
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)))
}))])
}
}
+55
View File
@@ -0,0 +1,55 @@
//! # Effect hooks module
//!
//! Provides React-like use_effect hooks for managing effects.
//! This module includes use_effect and use_sync_effect.
use std::sync::{Arc, Mutex};
use super::{state::State, HookDependency, HookEffect};
use crate::component::context::Context;
/// 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());
}
}
/// 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,
);
}
+44
View File
@@ -0,0 +1,44 @@
//! # State Management and Hooks Module
//!
//! Provides React-like hooks for managing component state and side effects.
//! This module includes useState, useEffect, useMount, and state synchronization hooks.
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
sync::{Arc, Mutex},
};
mod effect;
mod mount;
mod state;
pub use effect::*;
pub use mount::*;
pub use state::*;
/// Effect callback that can be triggered by state changes
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// Trait for values that can be tracked as dependencies in hooks
pub trait HookDependency: Send + Sync {
/// Register an effect to be triggered on updates
fn on_update(&self, hook: 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())()
}
}
impl Debug for HookEffect {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "HookEffect")
}
}
+58
View File
@@ -0,0 +1,58 @@
//! # Mount hooks module
//!
//! Provides React-like Mount hooks for managing effects.
//! This module includes Mount, use_mount and use_mount_manual.
use std::{
fmt::Debug,
sync::{Arc, Mutex},
};
use super::{HookDependency, HookEffect};
/// 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 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())),
)
}
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();
}
}
+152
View File
@@ -0,0 +1,152 @@
//! # State hooks module
//!
//! Provides React-like use_state hooks for managing states.
//! This module includes use_state and use_sync_state.
use std::{
any::Any,
fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard},
};
use super::{HookDependency, HookEffect};
use crate::component::context::Context;
/// 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)]
pub struct State<T> {
/// The actual state value
value: Arc<Mutex<T>>,
/// 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,
}
impl<T: Clone> State<T> {
/// 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 {
self.value.lock().unwrap().clone()
}
}
impl<T> State<T> {
/// Acquires a lock on the state for read/write access
///
/// 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 state value and triggers dependents
pub fn set(&self, v: T) {
*self.value.lock().unwrap() = v;
self.update();
}
/// Notifies all dependents of an update
pub fn update(&self) {
for d in self.dependents.lock().unwrap().iter() {
d.call();
}
}
/// Clones the State handle (not the value)
pub fn clone(&self) -> Self {
Self {
dependents: self.dependents.clone(),
value: self.value.clone(),
}
}
}
impl<T: Display> Display for State<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.value.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;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for Inner<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.updated = true;
&mut self.value
}
}
impl<T: Send + Sync> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook);
}
}
/// 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
}
/// 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> {
State {
value: Arc::new(Mutex::new(v)),
dependents: Arc::new(Mutex::new(Vec::new())),
}
}
+81 -526
View File
@@ -1,543 +1,98 @@
//! # OSUI //! # OSUI - A TUI Library for Advanced UIs
//! //!
//! A terminal user interface (TUI) library providing customizable components //! OSUI is a Rust library for building sophisticated Terminal User Interfaces (TUIs).
//! to build command-line interfaces in Rust. OSUI enables users to create //! It provides a component-based architecture with state management, event handling,
//! interactive CLI applications with various UI elements and handle keyboard //! and rendering capabilities for creating interactive console applications.
//! input for real-time events.
//! //!
//! ## Example Usage //! ## Key Features
//! //!
//! ```rust //! - **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 osui::prelude::*;
//! use std::sync::Arc;
//! //!
//! #[component] //! #[component]
//! pub fn App() -> Element { //! pub fn Counter(cx: &Arc<Context>) -> View {
//! let count = State::new(0); //! let count = use_state(0);
//! //!
//! rsx! { //! Arc::new(move |ctx| {
//! button { //! ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {}", count.get_dl()));
//! class: "btn", //! })
//!
//! on_click: fn(_, _, _) @count {
//! count += 1;
//! },
//!
//! "The current count is: {count}"
//! }
//! }
//! }
//! fn main() {
//! launch!(App);
//! } //! }
//! ``` //! ```
//!
//! ## Modules
//! - `ui` - Contains all user interface components, enabling rich CLI experiences.
//! - `utils` - Utility functions for common TUI tasks such as clearing the screen.
use std::{ use std::sync::Arc;
collections::HashMap,
fmt::Debug,
ops::{AddAssign, SubAssign},
sync::{Arc, Mutex},
};
use crossterm::event::Event; use crate::render::DrawContext;
use ui::{Style, StyleCore};
use utils::VisibleLength;
pub mod css; pub mod component;
pub mod examples; pub mod engine;
pub mod macros; pub mod frontend;
pub mod rsx; pub mod hooks;
pub mod ui; pub mod render;
pub mod utils; pub mod view_plugins;
pub mod prelude { pub mod prelude {
pub use crate::ui::Color::*; //! Prelude module - Re-exports commonly used items for convenience
pub use crate::ui::Font::*; pub use crate::component::components::*;
pub use crate::ui::Number::{Auto, Center}; pub use crate::component_prelude::*;
pub use crate::Instruction::*; }
pub use crate::{self as osui, css, ersx, launch, rsx, ui::*, Handler};
pub use crate::{style, Component, Document}; pub mod component_prelude {
pub use crate::{Children, Element, ElementCore, ElementWidget}; //! Prelude module - Re-exports commonly used items for convenience
pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; pub use crate::component::{context::*, scope::*, *};
pub use osui_element::component; pub use crate::engine::*;
pub fn sleep(ms: u64) { pub use crate::frontend::*;
std::thread::sleep(std::time::Duration::from_millis(ms)); pub use crate::hooks::*;
} pub use crate::render::*;
pub fn use_state<'a, T>(v: T) -> crate::State<T> { pub use crate::view_plugins::*;
crate::State::new(v) pub use crate::{sleep, Error, Result, View, ViewWrapper};
pub use crossterm;
pub use osui_macros::{component, rsx};
pub use std::sync::{Arc, Mutex};
}
/// A View is an async function that renders content to a DrawContext.
/// It takes a mutable DrawContext and produces drawing instructions.
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,
}
impl From<std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>> for Error {
fn from(_value: std::sync::PoisonError<std::sync::MutexGuard<'_, bool>>) -> Self {
Error::PoisonError
} }
} }
////////////////////////////////////////////////////////////////////////////////////////////////// /// Sleep for the specified duration in milliseconds.
// Type aliases /// Useful for controlling render frame rate or delays.
////////////////////////////////////////////////////////////////////////////////////////////////// pub fn sleep(delay_ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
pub type Element = Box<dyn ElementWidget>;
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Traits
//////////////////////////////////////////////////////////////////////////////////////////////////
pub trait ElementCore {
fn get_element_by_id(&mut self, id: &str) -> Option<&mut Element>;
fn get_id(&self) -> String;
fn get_class(&self) -> String;
fn get_style(&self) -> &Style;
}
pub trait ElementWidget: ElementCore + std::fmt::Debug {
fn render(&self, writer: &mut Writer);
fn event(&mut self, event: crossterm::event::Event, document: &mut Document) {
_ = (event, document)
}
fn initialize(&mut self, document: &mut Document) {
_ = document
}
}
pub trait Component: std::fmt::Debug {
type Element;
fn create_element(self) -> Self::Element;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Structs
//////////////////////////////////////////////////////////////////////////////////////////////////
pub struct Handler<T>(pub Arc<Mutex<dyn FnMut(&mut T, crossterm::event::Event, &mut Document)>>);
pub struct Document {
element: *mut Element,
running: Option<bool>,
pub css: ui::Css,
}
#[derive(Debug)]
pub struct Writer {
css: *const ui::Css,
style: ui::StyleElement,
focused: bool,
size: (u16, u16),
pos: (u16, u16),
written: (u16, u16),
parent: Option<*mut Writer>,
}
pub struct State<T>(Arc<Mutex<T>>);
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Enums
//////////////////////////////////////////////////////////////////////////////////////////////////
pub enum Children {
None,
Text(Arc<dyn Fn() -> String>),
StaticText(String),
Children(Vec<Element>, usize),
}
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Implementations
//////////////////////////////////////////////////////////////////////////////////////////////////
impl Document {
pub fn with_elem(element: &mut Element) -> Document {
Document {
element,
running: None,
css: HashMap::new(),
}
}
pub fn exit(&mut self) {
self.running = Some(false);
}
pub fn restart(&mut self) {
self.running = Some(true);
}
pub fn get_root<T>(&self) -> &mut Box<T> {
unsafe { &mut *(self.element as *mut Element as *mut Box<T>) }
}
pub fn get_element_by_id<T>(&self, id: &str) -> Option<&mut Box<T>> {
if let Some(e) = self.get_element_by_id_raw(id) {
Some(unsafe { &mut *(e as *mut Element as *mut Box<T>) })
} else {
None
}
}
pub fn get_element_by_id_raw(&self, id: &str) -> Option<&mut Element> {
if !self.element.is_null() {
unsafe {
if (*self.element).get_id() == id {
Some(&mut *self.element)
} else {
(*self.element).get_element_by_id(id)
}
}
} else {
None
}
}
pub fn render(&self) {
if !self.element.is_null() {
let (width, height) = crossterm::terminal::size().unwrap();
let mut writer = Writer {
css: &self.css,
focused: true,
style: ui::StyleElement {
// outline: (true, true),
..Default::default()
},
size: (width, height),
pos: (1, 1),
written: (0, 0),
parent: None,
};
let mut frame = writer.new_frame();
utils::clear();
frame.render(true, unsafe { &*self.element });
writer.after_render(width, height);
utils::flush();
}
}
pub fn set_css(&mut self, css: ui::Css) {
self.css = css;
}
pub fn clear_css(&mut self) {
self.css.clear();
}
pub fn draw(&mut self, element: Element) {
unsafe {
*self.element = element;
(*self.element).initialize(self);
}
}
pub fn run(&mut self) -> bool {
// Set up the screen
utils::hide_cursor();
crossterm::terminal::enable_raw_mode().unwrap();
utils::clear();
self.running = None;
let element = unsafe { &mut *self.element };
self.render();
element.initialize(self);
// Send initial event
element.event(crossterm::event::Event::FocusGained, self);
while self.running == None {
self.render();
element.event(crossterm::event::read().unwrap(), self);
}
utils::show_cursor();
crossterm::terminal::disable_raw_mode().unwrap();
utils::clear();
self.running.unwrap()
}
}
impl Default for Children {
fn default() -> Self {
Self::None
}
}
impl std::fmt::Debug for Children {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
Self::None => String::new(),
Self::Children(a, b) => format!("({b}, {a:?})"),
Self::Text(a) => format!("{:?}", a()),
Self::StaticText(a) => format!("static({a:?})"),
})
}
}
impl Children {
pub fn is_none(&self) -> bool {
match self {
Children::None => true,
_ => false,
}
}
pub fn get_text(&self) -> String {
match self {
Children::Text(text) => text(),
Children::StaticText(text) => text.clone(),
_ => String::new(),
}
}
pub fn set_text(&mut self, text: Arc<dyn Fn() -> String>) {
match self {
Children::Text(t) => {
*t = text;
}
_ => {}
}
}
pub fn set_static(&mut self, text: String) {
*self = Children::StaticText(text);
}
pub fn add_child(&mut self, element: Element) {
if let Children::Children(children, _) = self {
children.push(element);
}
}
pub fn get_child(&mut self) -> (Option<&mut Element>, usize) {
if let Children::Children(children, child) = self {
let l = children.len();
(children.get_mut(*child), l)
} else {
(None, 0)
}
}
pub fn get_child_idx(&mut self) -> (Option<(&mut Element, &mut usize)>, usize) {
if let Children::Children(children, child) = self {
let l = children.len();
if let Some(d) = children.get_mut(*child) {
(Some((d, child)), l)
} else {
(None, 0)
}
} else {
(None, 0)
}
}
pub fn set_index(&mut self, index: usize) {
if let Children::Children(_, child) = self {
*child = index;
}
}
pub fn len(&self) -> usize {
if let Children::Children(children, _) = self {
children.len()
} else {
0
}
}
pub fn index(&self) -> usize {
if let Children::Children(_, c) = self {
*c
} else {
0
}
}
pub fn index_mut(&mut self) -> Option<&mut usize> {
if let Children::Children(_, c) = self {
Some(c)
} else {
None
}
}
pub fn insert(&mut self, index: usize, element: Element) {
if let Children::Children(children, _) = self {
children.insert(index, element);
}
}
}
impl<T> Handler<T> {
pub fn new<F>(handler_fn: F) -> Handler<T>
where
F: FnMut(&mut T, crossterm::event::Event, &mut Document) + 'static,
{
Handler(Arc::new(Mutex::new(handler_fn)))
}
pub fn call(&self, s: &mut T, event: Event, document: &mut Document) {
let mut o = self.0.lock().unwrap();
o(s, event, document);
}
pub fn clone(&self) -> Handler<T> {
Handler(Arc::clone(&self.0))
}
}
impl Writer {
pub fn after_render(&self, width: u16, height: u16) {
if self.style.outline.1 {
let ol = self.style.write_outline("│");
let hl = "─".repeat(width as usize);
self.write_abs_outline(&self.style.write_outline(&format!("╭{}╮", hl)), (0, 0));
for i in 1..height+1 {
self.write_abs_outline(&ol, (0, i));
self.write_abs_outline(&ol, (width + 1, i));
}
self.write_abs_outline(&self.style.write_outline(&format!("╰{}╯", hl)), (0, height + 1));
}
}
pub fn write(&mut self, s: &str) {
self.write_abs(&self.style.write(&s), (0, 0));
}
pub fn write_abs_outline(&self, s: &str, pos: (u16, u16)) {
if let Some(parent) = self.parent {
let parent = unsafe { &mut *parent };
parent.write_abs_outline(s, (self.pos.0 + pos.0, self.pos.1 + pos.1));
} else {
print!(
"\x1b[{};{}H{}",
self.pos.1 + pos.1 - 1,
self.pos.0 + pos.0 - 1,
self.style.write_outline(s)
);
}
}
pub fn write_abs(&mut self, s: &str, pos: (u16, u16)) {
let width = self.get_size().0 as usize;
let mut max_written = self.written.0;
let mut lines_written = 0;
let truncated_lines: Vec<_> = s
.lines()
.map(|l| {
let truncated = if l.len() > width {
l.chars().take(width).collect::<String>()
} else {
l.to_string()
};
max_written = max_written.max(truncated.visible_len() as u16);
lines_written += 1;
truncated
})
.collect();
self.written.0 = max_written;
self.written.1 += lines_written;
if let Some(parent) = self.parent {
let parent = unsafe { &mut *parent };
let combined_pos = (self.pos.0 + pos.0, self.pos.1 + pos.1);
let s_combined = truncated_lines.join("\n");
parent.write_abs(&s_combined, combined_pos);
} else {
for (i, line) in truncated_lines.iter().enumerate() {
print!(
"\x1b[{};{}H{}",
self.pos.1 + pos.1 + i as u16,
self.pos.0 + pos.0,
line
);
}
}
}
pub fn get_focused(&self) -> bool {
self.focused
}
pub fn get_size(&self) -> (u16, u16) {
(
self.style
.width
.1
.as_size(self.size.0, self.size.0, self.style.outline.1),
self.style
.height
.1
.as_size(self.size.1, self.size.1, self.style.outline.1),
)
}
pub fn get_size_root(&self) -> (u16, u16) {
(
self.style
.width
.1
.as_size(self.written.0, self.size.0, self.style.outline.1),
self.style
.height
.1
.as_size(self.written.1, self.size.1, self.style.outline.1),
)
}
pub fn new_frame(&mut self) -> crate::utils::Frame {
crate::utils::Frame::new(self)
}
pub fn caret(&self) -> String {
format!(
"{}{}\x1b[0m",
self.style.cursor.1.ansi(),
self.style.caret.1
)
}
}
impl<T> Default for Handler<T> {
fn default() -> Self {
Handler(Arc::new(Mutex::new(
|_: &mut T, _: crossterm::event::Event, _: &mut Document| {},
)))
}
}
impl<T> std::fmt::Debug for Handler<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Handler()")
}
}
impl<T> State<T> {
pub fn new<'a>(v: T) -> State<T> {
State(Arc::new(Mutex::new(v)))
}
pub fn use_state<'a>(&'a self) -> std::sync::MutexGuard<'a, T> {
self.0.lock().unwrap()
}
pub fn clone<'a>(&'a self) -> State<T> {
State(Arc::clone(&self.0))
}
}
impl<T: std::fmt::Display> std::fmt::Display for State<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Acquire the lock, handle potential poisoning
match self.0.lock() {
Ok(guard) => write!(f, "{}", *guard),
Err(poisoned) => write!(f, "<Poisoned: {}>", *poisoned.into_inner()),
}
}
}
impl<T: AddAssign> AddAssign<T> for State<T> {
fn add_assign(&mut self, rhs: T) {
*self.0.lock().unwrap() += rhs;
}
}
impl<T: SubAssign> SubAssign<T> for State<T> {
fn sub_assign(&mut self, rhs: T) {
*self.0.lock().unwrap() -= rhs;
}
}
impl<T: PartialEq> PartialEq<T> for State<T> {
fn eq(&self, other: &T) -> bool {
self.use_state().eq(other)
}
fn ne(&self, other: &T) -> bool {
self.use_state().ne(other)
}
}
#[derive(Debug)]
pub enum Instruction<'a> {
SetStyle(ui::Css),
SetChild(usize),
Ghost(usize),
Load(Handler<ui::Div<'a>>),
} }
-18
View File
@@ -1,18 +0,0 @@
#[macro_export]
macro_rules! launch {
($elem:path) => {
let mut element = <$elem>::default().create_element();
let mut document = $crate::Document::with_elem(&mut element);
while document.run() {
element = <$elem>::default().create_element();
}
};
($elem:path, $css:expr) => {
let mut element = <$elem>::default().create_element();
let mut document = $crate::Document::with_elem(&mut element);
document.set_css($css);
while document.run() {
element = <$elem>::default().create_element();
}
};
}
-27
View File
@@ -1,27 +0,0 @@
use osui::{examples, prelude::*};
fn main() {
launch!(App);
}
#[component]
fn App() {
rsx! {
@SetStyle(css! {
example: "hover" {
color: Blue,
}
})
%text { "Choose an example to run" }
button { class: "example", on_click: |_, _, doc| {
doc.draw(examples::counter_example::App::default().create_element());
}, "Counter" }
button { class: "example", on_click: |_, _, doc| {
doc.draw(examples::login_example::App::default().create_element());
}, "Login" }
button { class: "example", on_click: |_, _, doc| {
doc.draw(examples::todo_example::App::default().create_element());
}, "Todo" }
}
}
+134
View File
@@ -0,0 +1,134 @@
//! # 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(Debug, 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(Debug, 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(Debug, 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: 0,
y: 0,
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 = x;
self.allocated.y = y;
self.allocated.width = width;
self.allocated.height = 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));
}
/// Clears drawing
pub fn clear(&mut self) {
self.drawing.clear();
}
}
impl Area {
fn right(&self) -> u16 {
self.x + self.width
}
fn bottom(&self) -> u16 {
self.y + self.height
}
pub fn merge(&mut self, other: &Self) {
self.x = self.x.min(other.x);
self.y = self.y.min(other.y);
self.width = self.right().max(other.right()) - self.x;
self.height = self.bottom().max(other.bottom()) - self.y;
}
}
-153
View File
@@ -1,153 +0,0 @@
#[macro_export]
macro_rules! check_expr {
(|| $code:block) => {
$crate::Handler::new(move |_, _, _| $code)
};
(|$($inner:tt)*| $code:block) => {
$crate::Handler::new(move |$($inner)*| $code)
};
($expr:expr) => {
$expr
};
}
#[macro_export]
macro_rules! parse_rsx_param {
//// FUNCTIONS
($elem:expr, $($k:ident).+: fn($($params:tt)*) $(@$($v:ident),+)? $body:expr $(, $($rest:tt)*)?) => {
$elem.$($k).+ = $crate::Handler::new({
$($(
#[allow(unused_mut)]
let mut $v = $v.clone();
)+)?
move |$($params)*| {
$body
}
});
osui::parse_rsx_param!($elem, $($($rest)*)?);
};
($elem:expr, $($k:ident).+: |$($args:tt $(: $aty:ty)?),*| $(@$($v:ident),+)? $body:expr $(, $($rest:tt)*)?) => {
$elem.$($k).+ = $crate::Handler::new({
$($(
#[allow(unused_mut)]
let mut $v = $v.clone();
)+)?
move |$($args $(: $aty)?),*| $body
});
osui::parse_rsx_param!($elem, $($($rest)*)?);
};
//// FUNCTIONS END
($elem:expr, async $code:block $($rest:tt)*) => {
let elem_len = $elem.children.len();
$elem.instructions.push($crate::Instruction::Load(Handler::new(
move |d: &mut Div, _, _| {
d.children.insert(elem_len, $code);
}
)));
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, %$elem_path:path { $($inner:tt)* } $($rest:tt)*) => {
$elem.ghosts.push($elem.ghosts.len());
if let $crate::Children::Children(children, _) = &mut $elem.children {
children.push($crate::ersx!($elem_path { $($inner)* }));
} else {
$elem.children = $crate::Children::Children(vec![$crate::ersx!($elem_path { $($inner)* })], 0)
}
osui::parse_rsx_param!($elem, $($rest)*)
};
($elem:expr, @ $($i:ident)::+($($inner:tt)*) $($rest:tt)*) => {
$elem.instructions.push($($i)::+($($inner)*));
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, @ $($i:ident)::+ $($rest:tt)*) => {
$elem.instructions.push($($i)::+);
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, static $text:literal $($other:tt)*) => {
$elem.children = $crate::Children::StaticText(format!($text $($other)*));
};
($elem:expr, for ($($for:tt)*) $code:block $($rest:tt)*) => {
if $elem.children.is_none() {$elem.children = $crate::Children::Children(Vec::new(), 0)}
if let $crate::Children::Children(children, _) = &mut $elem.children {
for $($for)* {
children.push($code)
}
}
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, $($k:ident).+: $v:expr $(, $($rest:tt)*)?) => {
$elem.$($k).+ = $crate::check_expr!($v);
osui::parse_rsx_param!($elem, $($($rest)*)?);
};
($elem:expr, $p:path) => {
$p;
};
($elem:expr, $($k:ident).+, $($rest:tt)*) => {
$elem.$($k).+ = !$elem.$($k).+;
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, $code:block $($rest:tt)*) => {
if $elem.children.is_none() {$elem.children = $crate::Children::Children(Vec::new(), 0)}
if let $crate::Children::Children(children, _) = &mut $elem.children {
children.push($code);
}
osui::parse_rsx_param!($elem, $($rest)*);
};
($elem:expr, $elem_path:path { $($inner:tt)* } $($rest:tt)*) => {
if let $crate::Children::Children(children, _) = &mut $elem.children {
children.push($crate::ersx!($elem_path { $($inner)* }));
} else {
$elem.children = $crate::Children::Children(vec![$crate::ersx!($elem_path { $($inner)* })], 0)
}
osui::parse_rsx_param!($elem, $($rest)*)
};
($elem:expr, $text:literal $($other:tt)*) => {
$elem.children = $crate::Children::Text(
std::sync::Arc::new(move || format!($text $($other)*))
);
};
($elem:expr,) => {};
}
#[macro_export]
macro_rules! ersx {
($elem:path { $($inner:tt)* }) => {{
let mut elem = <$elem>::default();
$crate::parse_rsx_param!(elem, $($inner)*);
elem.create_element()
}};
}
/// Makes a div and puts elements into it
///
/// # Example
/// ```
/// rsx! {
/// button { class: "btn", "Click me!" }
/// }
/// ```
///
/// # Returns
/// A `osui::Element` - Which is a `Box<dyn osui::ElementWidget>`
#[macro_export]
macro_rules! rsx {
($($inner:tt)*) => {
$crate::ersx!{ $crate::ui::div { $($inner)* } }
};
}
-267
View File
@@ -1,267 +0,0 @@
/// The `ui` module defines the basic building blocks for creating a terminal-based UI framework.
///
/// # Modules
/// - `styles`: A module for defining UI styles.
pub mod styles;
pub use styles::*;
use crate::prelude::*;
use osui_element::element;
#[element]
#[derive(Default, Debug)]
pub struct Text {}
impl ElementWidget for Text<'_> {
fn render(&self, writer: &mut crate::Writer) {
writer.write(&self.children.get_text());
}
}
#[component]
pub fn text<'a>(class: &'a str, id: &'a str, children: Children, style: Style) -> Box<Text<'a>> {
Box::new(Text {
class: self.class,
id: self.id,
children: self.children,
style: self.style,
})
}
#[element]
#[derive(Default, Debug)]
pub struct Div {
pub instructions: Vec<crate::Instruction<'a>>,
pub ghosts: Vec<usize>,
}
impl ElementWidget for Div<'_> {
fn render(&self, writer: &mut crate::Writer) {
let focused = writer.get_focused();
let mut frame = writer.new_frame();
if let Children::Children(children, child) = &self.children {
for (i, elem) in children.iter().enumerate() {
frame.render(focused && i == *child, elem);
}
}
}
fn event(&mut self, event: Event, document: &mut Document) {
if let (Some((child, i)), l) = self.children.get_child_idx() {
match event {
Event::Key(k) => match k.code {
KeyCode::Tab => {
child.event(Event::FocusLost, document);
if *i < l - 1 {
*i += 1;
} else {
*i = 0;
}
while self.ghosts.get(*i).is_some() {
if *i < l - 1 {
*i += 1;
} else {
*i = 0;
}
}
child.event(Event::FocusGained, document);
}
KeyCode::BackTab => {
child.event(Event::FocusLost, document);
if *i > 0 {
*i -= 1;
} else {
*i = l - 1;
}
while self.ghosts.get(*i).is_some() {
if *i > 0 {
*i -= 1;
} else {
*i = l - 1;
}
}
child.event(Event::FocusGained, document);
}
_ => child.event(event, document),
},
_ => child.event(event, document),
}
}
}
fn initialize(&mut self, document: &mut Document) {
let l = self.children.len();
if let Some(i) = self.children.index_mut() {
while self.ghosts.get(*i).is_some() {
if *i < l - 1 {
*i += 1;
} else {
*i = 0;
break;
}
}
}
let mut load_funcs = Vec::new();
for inst in &self.instructions {
match inst {
crate::Instruction::SetStyle(s) => {
document.css.extend(s.clone().into_iter());
}
crate::Instruction::SetChild(c) => {
self.children.set_index(*c);
}
crate::Instruction::Ghost(idx) => {
self.ghosts.push(*idx);
}
crate::Instruction::Load(f) => {
load_funcs.push(f.clone());
}
}
}
self.instructions.clear();
for i in load_funcs {
i.call(self, Event::FocusGained, document)
}
}
}
#[component]
pub fn div<'a>(
class: &'a str,
id: &'a str,
children: Children,
style: Style,
instructions: Vec<crate::Instruction<'a>>,
ghosts: Vec<usize>,
) -> Box<Div<'a>> {
Box::new(Div {
children: self.children,
class: self.class,
id: self.id,
style: self.style,
instructions: self.instructions,
ghosts: self.ghosts,
})
}
#[element]
#[derive(Default, Debug)]
pub struct Button {
pub on_click: Handler<Button<'a>>,
pub on_hover: Handler<Button<'a>>,
}
impl ElementWidget for Button<'_> {
fn render(&self, writer: &mut crate::Writer) {
writer.write(&self.children.get_text());
}
fn event(&mut self, event: Event, document: &mut Document) {
match event {
Event::Key(key) => {
if key.code == KeyCode::Enter {
document.render();
self.style.set_state("clicked");
self.on_click.clone().call(self, event, document);
document.render();
sleep(65);
self.style.set_state("");
}
}
Event::FocusGained => {
self.on_hover.clone().call(self, event, document);
}
_ => {}
}
}
}
#[component]
pub fn button<'a>(
class: &'a str,
id: &'a str,
children: Children,
style: Style,
on_click: Handler<Button<'a>>,
on_hover: Handler<Button<'a>>,
) -> Box<Button<'a>> {
Box::new(Button {
class: self.class,
id: self.id,
children: self.children,
on_click: self.on_click.clone(),
on_hover: self.on_hover.clone(),
style: self.style,
})
}
#[element]
#[derive(Default, Debug)]
pub struct Input {
pub on_click: Handler<Input<'a>>,
pub text: String,
pub placeholder: &'a str,
}
impl ElementWidget for Input<'_> {
fn render(&self, writer: &mut crate::Writer) {
if self.text.is_empty() {
writer.write(&format!(
"{}{}{}",
self.children.get_text(),
self.placeholder,
writer.caret()
));
} else {
writer.write(&format!(
"{}{}{}",
self.children.get_text(),
self.text,
writer.caret()
));
}
}
fn event(&mut self, event: crossterm::event::Event, document: &mut Document) {
match event {
Event::Key(k) => match k.code {
KeyCode::Backspace => {
if self.text.len() > 0 {
self.text.pop();
}
}
KeyCode::Enter => {
self.on_click.clone().call(self, event, document);
}
KeyCode::Char(c) => {
self.text.push(c);
}
_ => {}
},
_ => {}
}
}
}
#[component]
pub fn input<'a>(
class: &'a str,
id: &'a str,
placeholder: &'a str,
text: String,
children: Children,
style: Style,
on_click: Handler<Input<'a>>,
) -> Box<Input<'a>> {
Box::new(Input {
class: self.class,
id: self.id,
children: self.children,
style: self.style,
on_click: self.on_click,
placeholder: self.placeholder,
text: self.text,
})
}
-347
View File
@@ -1,347 +0,0 @@
//! The `styles` module defines various styling options for UI elements. This includes colors,
//! fonts, padding, margins, and other visual properties that can be applied to UI components
//! to change their appearance.
use std::{collections::HashMap, fmt::Debug};
use dyn_clone::{clone_trait_object, DynClone};
//////////////////////////////////////////////////////////////////////////////////////////////////
// Type aliases
//////////////////////////////////////////////////////////////////////////////////////////////////
pub type Css = HashMap<StyleName, StyleElement>;
//////////////////////////////////////////////////////////////////////////////////////////////////
// Traits
//////////////////////////////////////////////////////////////////////////////////////////////////
pub trait StyleCore: Debug + Send + Sync + DynClone {
fn ansi(&self) -> String;
fn ansi_bg(&self) -> String;
}
clone_trait_object!(StyleCore);
//////////////////////////////////////////////////////////////////////////////////////////////////
// Enums
//////////////////////////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Font {
Bold,
Underline,
Italic,
Reverse,
Strike,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Color {
NoColor,
Rgb(u8, u8, u8),
Hex(String),
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Number {
Px(u16),
Pe(u16),
Center,
Auto,
}
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub enum StyleName {
Class(String),
ClassState(String, String),
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// Structs
//////////////////////////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Clone)]
pub struct StyleElement {
pub color: (bool, Color),
pub background: (bool, Color),
pub outline_color: (bool, Color),
pub font: (bool, Vec<Font>),
pub x: (bool, Number),
pub y: (bool, Number),
pub width: (bool, Number),
pub height: (bool, Number),
pub visible: (bool, bool),
pub outline: (bool, bool),
pub cursor: (bool, Color),
pub caret: (bool, String),
}
#[derive(Debug, Clone)]
pub struct Style(
pub StyleElement,
pub HashMap<String, StyleElement>,
pub String,
);
//////////////////////////////////////////////////////////////////////////////////////////////////
// Implementations
//////////////////////////////////////////////////////////////////////////////////////////////////
impl Default for Style {
fn default() -> Self {
Style(StyleElement::default(), HashMap::new(), String::new())
}
}
impl Style {
pub fn get(mut self, hover: bool) -> StyleElement {
if let Some(style_element) = self.1.get(&self.2) {
self.0.merge(style_element);
self.0
} else if hover {
if let Some(style_element) = self.1.get("hover") {
self.0.merge(style_element);
self.0
} else {
self.0
}
} else {
self.0
}
}
pub fn set_state(&mut self, state: &str) {
self.2 = state.to_string();
}
}
impl StyleElement {
pub fn write(&self, s: &str) -> String {
s.split('\n')
.into_iter()
.map(|p| {
format!(
"{}{}{}{p}\x1b[0m",
self.color.1.ansi(),
self.background.1.ansi_bg(),
self.font.1.ansi()
)
})
.collect::<Vec<String>>()
.join("\n")
}
pub fn write_outline(&self, outline: &str) -> String {
format!("{}{outline}\x1b[0m", self.outline_color.1.ansi(),)
}
}
impl Default for StyleElement {
fn default() -> Self {
StyleElement {
color: (false, Color::NoColor),
background: (false, Color::NoColor),
cursor: (false, Color::NoColor),
font: (false, Vec::new()),
outline_color: (false, Color::NoColor),
x: (false, Number::Px(0)),
y: (false, Number::Auto),
width: (false, Number::Auto),
height: (false, Number::Auto),
visible: (false, true),
outline: (false, false),
caret: (false, String::from("█")),
}
}
}
impl StyleElement {
pub fn merge(&mut self, upper: &Self) {
if upper.color.0 {
self.color = upper.color.clone();
}
if upper.background.0 {
self.background = upper.background.clone();
}
if upper.font.0 {
self.font = upper.font.clone();
}
if upper.outline_color.0 {
self.outline_color = upper.outline_color.clone();
}
if upper.x.0 {
self.x = upper.x.clone();
}
if upper.y.0 {
self.y = upper.y.clone();
}
if upper.width.0 {
self.width = upper.width.clone();
}
if upper.height.0 {
self.height = upper.height.clone();
}
if upper.visible.0 {
self.visible = upper.visible.clone();
}
if upper.outline.0 {
self.outline = upper.outline.clone();
}
if upper.cursor.0 {
self.cursor = upper.cursor.clone();
}
if upper.caret.0 {
self.caret = upper.caret.clone();
}
}
}
impl StyleCore for Color {
fn ansi(&self) -> String {
String::from(match self {
Color::NoColor => "",
Color::Rgb(r, g, b) => {
return format!("\x1b[38;2;{r};{g};{b}m");
}
Color::Hex(hex) => {
let (r, g, b) = hex_to_rgb(hex);
return format!("\x1b[38;2;{r};{g};{b}m");
}
Color::Black => "\x1b[30m",
Color::Red => "\x1b[31m",
Color::Green => "\x1b[32m",
Color::Yellow => "\x1b[33m",
Color::Blue => "\x1b[34m",
Color::Magenta => "\x1b[35m",
Color::Cyan => "\x1b[36m",
Color::White => "\x1b[37m",
})
}
fn ansi_bg(&self) -> String {
String::from(match self {
Color::NoColor => "",
Color::Rgb(r, g, b) => {
return format!("\x1b[48;2;{r};{g};{b}m");
}
Color::Hex(hex) => {
let (r, g, b) = hex_to_rgb(hex);
return format!("\x1b[48;2;{r};{g};{b}m");
}
Color::Black => "\x1b[40m",
Color::Red => "\x1b[41m",
Color::Green => "\x1b[42m",
Color::Yellow => "\x1b[43m",
Color::Blue => "\x1b[44m",
Color::Magenta => "\x1b[45m",
Color::Cyan => "\x1b[46m",
Color::White => "\x1b[47m",
})
}
}
impl Default for Color {
fn default() -> Self {
Color::NoColor
}
}
fn hex_to_rgb(hex: &str) -> (u8, u8, u8) {
let hex = hex.trim_start_matches('#');
let hex = if hex.len() == 3 {
format!(
"{}{}{}{}{}{}",
&hex[0..1],
&hex[0..1],
&hex[1..2],
&hex[1..2],
&hex[2..3],
&hex[2..3]
)
} else if hex.len() == 6 {
hex.to_string()
} else {
return (255, 255, 255);
};
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
(r, g, b)
}
impl StyleCore for Font {
fn ansi(&self) -> String {
String::from(match self {
Font::Bold => "\x1b[1m",
Font::Underline => "\x1b[4m",
Font::Italic => "\x1b[3m",
Font::Reverse => "\x1b[7m",
Font::Strike => "\x1b[9m",
})
}
fn ansi_bg(&self) -> String {
self.ansi()
}
}
impl StyleCore for Vec<Font> {
fn ansi(&self) -> String {
let mut a = String::new();
for i in self {
a += i.ansi().as_str();
}
a
}
fn ansi_bg(&self) -> String {
let mut a = String::new();
for i in self {
a += i.ansi_bg().as_str();
}
a
}
}
impl Number {
pub fn as_size_raw(&self, frame_size: u16) -> u16 {
match self {
Number::Px(s) => *s,
Number::Pe(pe) => (frame_size * pe) / 100,
_ => frame_size,
}
}
pub fn as_size(&self, written: u16, frame_size: u16, outline: bool) -> u16 {
if *self == Number::Auto {
written
} else if outline {
self.as_size_raw(frame_size) - 2
} else {
self.as_size_raw(frame_size)
}
}
pub fn as_position_raw(&self, used: &u16, frame_size: u16) -> u16 {
match self {
crate::ui::Number::Px(px) => *px,
crate::ui::Number::Pe(pe) => (frame_size * pe) / 100,
crate::ui::Number::Center => (frame_size) / 2,
crate::ui::Number::Auto => *used,
}
}
pub fn as_position(&self, outline: bool, used: &u16, frame_size: u16) -> u16 {
if outline {
self.as_position_raw(used, frame_size) + 1
} else {
self.as_position_raw(used, frame_size)
}
}
}
-133
View File
@@ -1,133 +0,0 @@
use std::io::{stdout, Write};
pub struct Frame<'a> {
used: Vec<u16>,
writer: &'a mut crate::Writer,
}
impl<'a> Frame<'a> {
pub fn new(writer: &'a mut crate::Writer) -> Self {
Frame {
used: vec![0; writer.size.1 as usize],
writer,
}
}
pub fn render(&mut self, focused: bool, element: &crate::Element) {
let style = element.get_style();
let mut style_element = style.clone().get(focused);
let css = unsafe { &*self.writer.css };
for class in element.get_class().split(" ") {
if let Some(upper) = css.get(&crate::ui::StyleName::Class(class.to_string())) {
style_element.merge(upper);
}
if let Some(upper) = css.get(&crate::ui::StyleName::ClassState(
class.to_string(),
if style.2 == "" && focused {
"hover".to_string()
} else {
style.2.clone()
},
)) {
style_element.merge(upper);
}
}
if !style_element.visible.1 {
return;
}
let y = style_element.y.1.as_position(
style_element.outline.1,
&{
let mut y = 0;
while y < self.used.len() && self.used[y] != 0 {
y += 1;
}
y as u16
},
self.writer.size.1,
);
if let Some(y_) = self.used.get(y as usize) {
let x = style_element
.x
.1
.as_position(style_element.outline.1, y_, self.writer.size.0);
let mut writer = crate::Writer {
focused,
style: style_element.clone(),
pos: (x, y),
size: self.writer.size,
css,
parent: Some(self.writer),
written: (0, 0),
};
element.render(&mut writer);
let (width, height) = writer.get_size_root();
{
let (x, y, width, height) = if style_element.outline.1 {
(x - 1, y - 1, width + 2, height + 2)
} else {
(x, y, width, height)
};
for i in y..y + height {
if let Some(o) = self.used.get_mut(i as usize) {
if style_element.x.1 != crate::ui::Number::Auto {
*o += x;
}
*o += width;
}
}
}
writer.after_render(width, height);
}
}
}
pub fn clear() {
print!("\x1B[2J\x1B[H");
stdout().flush().unwrap();
}
pub fn hide_cursor() {
print!("\x1b[?25l");
stdout().flush().unwrap();
}
pub fn show_cursor() {
print!("\x1B[?25h");
stdout().flush().unwrap();
}
pub fn flush() {
stdout().flush().unwrap();
}
pub trait VisibleLength {
fn visible_len(&self) -> usize;
}
impl VisibleLength for String {
fn visible_len(&self) -> usize {
let mut length = 0;
let mut in_escape = false;
for c in self.chars() {
if c == '\x1b' {
in_escape = true;
} else if in_escape {
if c.is_ascii_alphabetic() {
in_escape = false;
}
} else {
length += 1;
}
}
length
}
}
+185
View File
@@ -0,0 +1,185 @@
//! # `View` Plugins Module
//!
//! Provides the essential plugins for modifying a `View`
use crate::{
render::{DrawContext, DrawInstruction},
View,
};
/// Horizontally centers the allocated area within the available draw area.
///
/// # Behavior
/// - Modifies `ctx.allocated.x`
/// - Does **not** modify height or width
/// - Assumes `ctx.allocated.width <= ctx.area.width`
///
/// # Notes
/// This function only affects positioning, not sizing.
pub fn x_center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Vertically centers the allocated area within the available draw area.
///
/// # Behavior
/// - Modifies `ctx.allocated.y`
/// - Does **not** modify width or height
/// - Assumes `ctx.allocated.height <= ctx.area.height`
///
/// # Typical usage
/// Called after size has been resolved (e.g. after `size_auto`,
/// `height_auto`, or a fixed height has been set).
pub fn y_center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Centers the allocated area both horizontally and vertically.
///
/// # Behavior
/// - Modifies `ctx.allocated.x` and `ctx.allocated.y`
/// - Does **not** modify width or height
///
/// # Order
/// Should generally be called **after** size resolution
/// (e.g. `size_auto`, `width_auto`, `height_auto`).
pub fn center(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.x = (ctx.area.width - ctx.allocated.width) / 2;
ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2;
}
/// Automatically computes both width and height based on drawn content.
///
/// # Sizing rules
/// - `Text`:
/// - Width = longest line length
/// - Height = number of lines
/// - `View`:
/// - Recursively executes the view in a fresh `DrawContext`
/// - Uses the child view's allocated size
/// - `Child`: ignored
///
/// # Notes
/// - This function performs a layout pass only.
/// - It does not draw anything.
/// - Later instructions may overwrite the computed size.
pub fn size_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.width = c.allocated.width;
ctx.allocated.height = c.allocated.height;
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes both width and height based on drawn content.
/// Merges the allocation for multiple drawings.
pub fn size_auto_merge(ctx: &mut DrawContext, _view: &View) {
ctx.allocated.x = u16::MAX;
ctx.allocated.y = u16::MAX;
ctx.allocated.width = 0;
ctx.allocated.height = 0;
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.merge(&c.allocated);
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes width based on drawn content.
///
/// # Sizing rules
/// - `Text`: width is the longest line
/// - `View`: width is taken from the child view's auto-sized result
/// - Height is left unchanged
///
/// # When to use
/// Useful when height is fixed or externally constrained.
pub fn width_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
for line in text.lines() {
ctx.allocated.width = ctx.allocated.width.max(line.len() as u16);
}
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.width = c.allocated.width;
}
DrawInstruction::Child(_, _) => {}
}
}
}
/// Automatically computes height based on drawn content.
///
/// # Sizing rules
/// - `Text`: height is the number of lines
/// - `View`: height is taken from the child view's auto-sized result
/// - Width is left unchanged
///
/// # Notes
/// This pass ignores line width and wrapping concerns.
pub fn height_auto(ctx: &mut DrawContext, _view: &View) {
for i in &ctx.drawing {
match i {
DrawInstruction::Text(_, text) => {
let mut height = 0;
for _ in text.lines() {
height += 1;
}
ctx.allocated.height = height;
}
DrawInstruction::View(area, view) => {
let mut c = DrawContext::new(area.clone());
view(&mut c);
size_auto(&mut c, view);
ctx.allocated.height = c.allocated.height;
}
DrawInstruction::Child(_, _) => {}
}
}
}
pub fn redraw(ctx: &mut DrawContext, view: &View) {
ctx.clear();
ctx.draw_view(ctx.allocated.clone(), view.clone());
}
-9
View File
@@ -1,9 +0,0 @@
`color`, `background`: `$color{}\x1b[0m`;
`font`: `$font{}\x1b[0m`;
`width`, `height`: `n`, `n%`, `auto`;
`x`, `y`: `n`, `n%`, `center`, `auto`;
<!-- `display`: `row`, `column`, `overlap`; -->