From 9f74c71045eeedbc9dccadd069f16eaa194328eb Mon Sep 17 00:00:00 2001 From: StarToaster <startoaster23@gmail.com> Date: Thu, 20 Oct 2022 14:00:12 -0400 Subject: [PATCH] New rewrite merged in! --- .gitignore | 2 +- .vscode/launch.json | 490 ++++++ Cargo.toml | 34 +- LICENSE | 2 +- bevy_kayak_renderer/Cargo.toml | 20 - bevy_kayak_ui/Cargo.toml | 19 - bevy_kayak_ui/src/bevy_context.rs | 87 -- bevy_kayak_ui/src/cursor.rs | 41 - bevy_kayak_ui/src/key.rs | 169 --- bevy_kayak_ui/src/lib.rs | 184 --- .../src/render/image/image_manager.rs | 72 - bevy_kayak_ui/src/render/image/mod.rs | 14 - examples/bevy_event.rs | 82 - examples/bevy_state.rs | 116 -- examples/clipping.rs | 110 +- examples/context.rs | 391 +++++ examples/counter.rs | 82 - examples/fold.rs | 170 --- examples/full_ui.rs | 164 -- examples/global_counter.rs | 69 - examples/hooks.rs | 152 -- examples/if.rs | 80 - examples/image.rs | 66 +- examples/nine_patch.rs | 106 +- examples/provider.rs | 304 ---- examples/quads.rs | 106 ++ examples/render_target.rs | 157 -- examples/scrollbox.rs | 77 - examples/scrolling.rs | 75 + examples/shrink_grow_layout.rs | 131 -- examples/simple.rs | 86 ++ examples/simple_state.rs | 122 ++ examples/tabs/tab.rs | 164 +- examples/tabs/tab_bar.rs | 68 - examples/tabs/tab_box.rs | 65 - examples/tabs/tab_button.rs | 91 ++ examples/tabs/tab_content.rs | 44 - examples/tabs/tab_context.rs | 54 + examples/tabs/tabs.rs | 145 +- examples/tabs/theming.rs | 37 - examples/text.rs | 90 ++ examples/text_box.rs | 202 ++- examples/texture_atlas.rs | 101 +- examples/todo/add_button.rs | 61 - examples/todo/card.rs | 49 - examples/todo/cards.rs | 26 - examples/todo/delete_button.rs | 61 - examples/todo/input.rs | 156 ++ examples/todo/items.rs | 107 ++ examples/todo/todo.rs | 154 +- examples/vec.rs | 88 ++ examples/vec_widget.rs | 50 - examples/widget_template.rs | 51 + examples/windows.rs | 57 - examples/world_interaction.rs | 274 ---- kayak_core/Cargo.toml | 31 - kayak_core/src/assets.rs | 96 -- kayak_core/src/binding.rs | 31 - kayak_core/src/children.rs | 31 - kayak_core/src/color.rs | 48 - kayak_core/src/context.rs | 821 ---------- kayak_core/src/context_ref.rs | 586 -------- kayak_core/src/cursor_icon.rs | 44 - kayak_core/src/flo_binding/CHANGELOG | 6 - kayak_core/src/flo_binding/LICENSE | 203 --- kayak_core/src/flo_binding/bind_stream.rs | 228 --- kayak_core/src/flo_binding/binding.rs | 239 --- kayak_core/src/flo_binding/binding_context.rs | 187 --- kayak_core/src/flo_binding/bindref.rs | 183 --- kayak_core/src/flo_binding/computed.rs | 347 ----- kayak_core/src/flo_binding/follow.rs | 249 ---- kayak_core/src/flo_binding/mod.rs | 736 --------- kayak_core/src/flo_binding/notify_fn.rs | 30 - kayak_core/src/flo_binding/releasable.rs | 135 -- .../src/flo_binding/rope_binding/core.rs | 125 -- .../src/flo_binding/rope_binding/mod.rs | 11 - .../flo_binding/rope_binding/rope_binding.rs | 255 ---- .../rope_binding/rope_binding_mut.rs | 287 ---- .../src/flo_binding/rope_binding/stream.rs | 161 -- .../flo_binding/rope_binding/stream_state.rs | 25 - .../src/flo_binding/rope_binding/tests.rs | 44 - kayak_core/src/flo_binding/traits.rs | 89 -- kayak_core/src/fragment.rs | 98 -- kayak_core/src/generational_arena.rs | 1323 ----------------- kayak_core/src/keys.rs | 203 --- kayak_core/src/layout.rs | 86 -- kayak_core/src/layout_dispatcher.rs | 54 - kayak_core/src/lib.rs | 118 -- kayak_core/src/lifetime.rs | 57 - kayak_core/src/multi_state.rs | 26 - kayak_core/src/node.rs | 465 ------ kayak_core/src/on_event.rs | 51 - kayak_core/src/on_layout.rs | 52 - kayak_core/src/styles/option_ref.rs | 30 - kayak_core/src/vec.rs | 132 -- kayak_core/src/widget.rs | 179 --- kayak_core/src/widget_manager.rs | 550 ------- kayak_font/Cargo.toml | 12 +- kayak_font/assets/roboto.png | Bin 98252 -> 0 bytes kayak_font/examples/bevy.rs | 19 +- kayak_font/examples/renderer/extract.rs | 3 +- kayak_font/examples/renderer/pipeline.rs | 7 +- kayak_font/src/bevy/renderer/extract.rs | 6 +- .../src/bevy/renderer/font_texture_cache.rs | 3 +- kayak_render_macros/examples/main.rs | 63 - kayak_render_macros/src/child.rs | 58 - kayak_render_macros/src/children.rs | 185 --- kayak_render_macros/src/function_component.rs | 135 -- kayak_render_macros/src/lib.rs | 267 ---- kayak_render_macros/src/partial_eq.rs | 16 - kayak_render_macros/src/use_effect.rs | 71 - kayak_render_macros/src/widget.rs | 126 -- kayak_render_macros/src/widget_props.rs | 156 -- .../Cargo.toml | 9 +- .../src/attribute.rs | 190 +-- kayak_ui_macros/src/block.rs | 499 +++++++ kayak_ui_macros/src/child.rs | 58 + kayak_ui_macros/src/children.rs | 215 +++ kayak_ui_macros/src/lib.rs | 77 + .../src/tags.rs | 23 +- kayak_ui_macros/src/widget.rs | 235 +++ .../src/widget_attributes.rs | 262 ++-- .../src/widget_builder.rs | 17 +- out.log | Bin 0 -> 158970 bytes src/calculate_nodes.rs | 321 ++++ .../src => src}/camera/camera.rs | 151 +- .../src => src}/camera/mod.rs | 46 +- .../src => src}/camera/ortho.rs | 142 +- src/children.rs | 45 + src/context.rs | 500 +++++++ src/context_entities.rs | 40 + {kayak_core/src => src}/cursor.rs | 94 +- {kayak_core/src => src}/event.rs | 35 +- {kayak_core/src => src}/event_dispatcher.rs | 581 +++++--- {kayak_core/src => src}/focus_tree.rs | 70 +- src/input.rs | 127 ++ {kayak_core/src => src}/input_event.rs | 86 +- .../src/keyboard.rs => src/keyboard_event.rs | 118 +- .../src/layout_cache.rs => src/layout.rs | 243 ++- src/layout_dispatcher.rs | 83 ++ src/lib.rs | 77 +- src/node.rs | 441 ++++++ src/on_change.rs | 79 + src/on_event.rs | 87 ++ src/on_layout.rs | 70 + .../render/mod.rs => src/render/extract.rs | 67 +- .../src => src}/render/font/extract.rs | 16 +- .../src => src}/render/font/font_mapping.rs | 213 +-- {bevy_kayak_ui/src => src}/render/font/mod.rs | 18 +- .../src => src}/render/image/extract.rs | 20 +- src/render/image/mod.rs | 2 + .../src => src}/render/mod.rs | 12 +- .../src => src}/render/nine_patch/extract.rs | 23 +- .../src => src}/render/nine_patch/mod.rs | 4 +- .../src => src}/render/quad/extract.rs | 13 +- {bevy_kayak_ui/src => src}/render/quad/mod.rs | 4 +- .../render/texture_atlas/extract.rs | 31 +- .../src => src}/render/texture_atlas/mod.rs | 4 +- .../src => src}/render/ui_pass.rs | 206 +-- .../src => src}/render/unified/mod.rs | 9 +- .../src => src}/render/unified/pipeline.rs | 9 +- .../src => src}/render/unified/shader.wgsl | 202 +-- .../src => src}/render/unified/text.rs | 66 +- {kayak_core/src => src}/render_primitive.rs | 32 +- {kayak_core/src => src}/styles/corner.rs | 468 +++--- {kayak_core/src => src}/styles/edge.rs | 416 +++--- src/styles/mod.rs | 11 + src/styles/options_ref.rs | 30 + .../src => src/styles}/render_command.rs | 66 +- .../src/styles/mod.rs => src/styles/style.rs | 90 +- {kayak_core/src => src}/tree.rs | 434 +++--- src/widget.rs | 7 + src/widget_context.rs | 185 +++ src/widgets/app.rs | 127 +- src/widgets/background.rs | 90 +- src/widgets/button.rs | 127 +- src/widgets/clip.rs | 98 +- src/widgets/element.rs | 94 +- src/widgets/fold.rs | 141 -- src/widgets/if_element.rs | 41 - src/widgets/image.rs | 81 +- src/widgets/inspector.rs | 114 -- src/widgets/mod.rs | 95 +- src/widgets/nine_patch.rs | 111 +- src/widgets/on_change.rs | 27 - src/widgets/scroll/mod.rs | 13 +- src/widgets/scroll/scroll_bar.rs | 551 +++---- src/widgets/scroll/scroll_box.rs | 420 +++--- src/widgets/scroll/scroll_content.rs | 157 +- src/widgets/scroll/scroll_context.rs | 63 +- src/widgets/spin_box.rs | 352 ----- src/widgets/text.rs | 107 +- src/widgets/text_box.rs | 302 ++-- src/widgets/texture_atlas.rs | 99 +- src/widgets/tooltip.rs | 275 ---- src/widgets/window.rs | 304 ++-- .../src/lib.rs => src/window_size.rs | 29 +- 197 files changed, 9512 insertions(+), 17739 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 bevy_kayak_renderer/Cargo.toml delete mode 100644 bevy_kayak_ui/Cargo.toml delete mode 100644 bevy_kayak_ui/src/bevy_context.rs delete mode 100644 bevy_kayak_ui/src/cursor.rs delete mode 100644 bevy_kayak_ui/src/key.rs delete mode 100644 bevy_kayak_ui/src/lib.rs delete mode 100644 bevy_kayak_ui/src/render/image/image_manager.rs delete mode 100644 bevy_kayak_ui/src/render/image/mod.rs delete mode 100644 examples/bevy_event.rs delete mode 100644 examples/bevy_state.rs create mode 100644 examples/context.rs delete mode 100644 examples/counter.rs delete mode 100644 examples/fold.rs delete mode 100644 examples/full_ui.rs delete mode 100644 examples/global_counter.rs delete mode 100644 examples/hooks.rs delete mode 100644 examples/if.rs delete mode 100644 examples/provider.rs create mode 100644 examples/quads.rs delete mode 100644 examples/render_target.rs delete mode 100644 examples/scrollbox.rs create mode 100644 examples/scrolling.rs delete mode 100644 examples/shrink_grow_layout.rs create mode 100644 examples/simple.rs create mode 100644 examples/simple_state.rs delete mode 100644 examples/tabs/tab_bar.rs delete mode 100644 examples/tabs/tab_box.rs create mode 100644 examples/tabs/tab_button.rs delete mode 100644 examples/tabs/tab_content.rs create mode 100644 examples/tabs/tab_context.rs delete mode 100644 examples/tabs/theming.rs create mode 100644 examples/text.rs delete mode 100644 examples/todo/add_button.rs delete mode 100644 examples/todo/card.rs delete mode 100644 examples/todo/cards.rs delete mode 100644 examples/todo/delete_button.rs create mode 100644 examples/todo/input.rs create mode 100644 examples/todo/items.rs create mode 100644 examples/vec.rs delete mode 100644 examples/vec_widget.rs create mode 100644 examples/widget_template.rs delete mode 100644 examples/windows.rs delete mode 100644 examples/world_interaction.rs delete mode 100644 kayak_core/Cargo.toml delete mode 100644 kayak_core/src/assets.rs delete mode 100644 kayak_core/src/binding.rs delete mode 100644 kayak_core/src/children.rs delete mode 100644 kayak_core/src/color.rs delete mode 100644 kayak_core/src/context.rs delete mode 100644 kayak_core/src/context_ref.rs delete mode 100644 kayak_core/src/cursor_icon.rs delete mode 100644 kayak_core/src/flo_binding/CHANGELOG delete mode 100644 kayak_core/src/flo_binding/LICENSE delete mode 100644 kayak_core/src/flo_binding/bind_stream.rs delete mode 100644 kayak_core/src/flo_binding/binding.rs delete mode 100644 kayak_core/src/flo_binding/binding_context.rs delete mode 100644 kayak_core/src/flo_binding/bindref.rs delete mode 100644 kayak_core/src/flo_binding/computed.rs delete mode 100644 kayak_core/src/flo_binding/follow.rs delete mode 100644 kayak_core/src/flo_binding/mod.rs delete mode 100644 kayak_core/src/flo_binding/notify_fn.rs delete mode 100644 kayak_core/src/flo_binding/releasable.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/core.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/mod.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/rope_binding.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/stream.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/stream_state.rs delete mode 100644 kayak_core/src/flo_binding/rope_binding/tests.rs delete mode 100644 kayak_core/src/flo_binding/traits.rs delete mode 100644 kayak_core/src/fragment.rs delete mode 100644 kayak_core/src/generational_arena.rs delete mode 100644 kayak_core/src/keys.rs delete mode 100644 kayak_core/src/layout.rs delete mode 100644 kayak_core/src/layout_dispatcher.rs delete mode 100644 kayak_core/src/lib.rs delete mode 100644 kayak_core/src/lifetime.rs delete mode 100644 kayak_core/src/multi_state.rs delete mode 100644 kayak_core/src/node.rs delete mode 100644 kayak_core/src/on_event.rs delete mode 100644 kayak_core/src/on_layout.rs delete mode 100644 kayak_core/src/styles/option_ref.rs delete mode 100644 kayak_core/src/vec.rs delete mode 100644 kayak_core/src/widget.rs delete mode 100644 kayak_core/src/widget_manager.rs delete mode 100644 kayak_font/assets/roboto.png delete mode 100644 kayak_render_macros/examples/main.rs delete mode 100644 kayak_render_macros/src/child.rs delete mode 100644 kayak_render_macros/src/children.rs delete mode 100644 kayak_render_macros/src/function_component.rs delete mode 100644 kayak_render_macros/src/lib.rs delete mode 100644 kayak_render_macros/src/partial_eq.rs delete mode 100644 kayak_render_macros/src/use_effect.rs delete mode 100644 kayak_render_macros/src/widget.rs delete mode 100644 kayak_render_macros/src/widget_props.rs rename {kayak_render_macros => kayak_ui_macros}/Cargo.toml (59%) rename {kayak_render_macros => kayak_ui_macros}/src/attribute.rs (95%) create mode 100644 kayak_ui_macros/src/block.rs create mode 100644 kayak_ui_macros/src/child.rs create mode 100644 kayak_ui_macros/src/children.rs create mode 100644 kayak_ui_macros/src/lib.rs rename {kayak_render_macros => kayak_ui_macros}/src/tags.rs (84%) create mode 100644 kayak_ui_macros/src/widget.rs rename {kayak_render_macros => kayak_ui_macros}/src/widget_attributes.rs (77%) rename {kayak_render_macros => kayak_ui_macros}/src/widget_builder.rs (66%) create mode 100644 out.log create mode 100644 src/calculate_nodes.rs rename {bevy_kayak_renderer/src => src}/camera/camera.rs (93%) rename {bevy_kayak_renderer/src => src}/camera/mod.rs (96%) rename {bevy_kayak_renderer/src => src}/camera/ortho.rs (86%) create mode 100644 src/children.rs create mode 100644 src/context.rs create mode 100644 src/context_entities.rs rename {kayak_core/src => src}/cursor.rs (96%) rename {kayak_core/src => src}/event.rs (85%) rename {kayak_core/src => src}/event_dispatcher.rs (53%) rename {kayak_core/src => src}/focus_tree.rs (81%) create mode 100644 src/input.rs rename {kayak_core/src => src}/input_event.rs (95%) rename kayak_core/src/keyboard.rs => src/keyboard_event.rs (95%) rename kayak_core/src/layout_cache.rs => src/layout.rs (52%) create mode 100644 src/layout_dispatcher.rs create mode 100644 src/node.rs create mode 100644 src/on_change.rs create mode 100644 src/on_event.rs create mode 100644 src/on_layout.rs rename bevy_kayak_ui/src/render/mod.rs => src/render/extract.rs (68%) rename {bevy_kayak_ui/src => src}/render/font/extract.rs (88%) rename {bevy_kayak_ui/src => src}/render/font/font_mapping.rs (70%) rename {bevy_kayak_ui/src => src}/render/font/mod.rs (61%) rename {bevy_kayak_ui/src => src}/render/image/extract.rs (73%) create mode 100644 src/render/image/mod.rs rename {bevy_kayak_renderer/src => src}/render/mod.rs (86%) rename {bevy_kayak_ui/src => src}/render/nine_patch/extract.rs (95%) rename {bevy_kayak_ui/src => src}/render/nine_patch/mod.rs (96%) rename {bevy_kayak_ui/src => src}/render/quad/extract.rs (91%) rename {bevy_kayak_ui/src => src}/render/quad/mod.rs (95%) rename {bevy_kayak_ui/src => src}/render/texture_atlas/extract.rs (72%) rename {bevy_kayak_ui/src => src}/render/texture_atlas/mod.rs (96%) rename {bevy_kayak_renderer/src => src}/render/ui_pass.rs (96%) rename {bevy_kayak_renderer/src => src}/render/unified/mod.rs (94%) rename {bevy_kayak_renderer/src => src}/render/unified/pipeline.rs (99%) rename {bevy_kayak_renderer/src => src}/render/unified/shader.wgsl (96%) rename {bevy_kayak_renderer/src => src}/render/unified/text.rs (96%) rename {kayak_core/src => src}/render_primitive.rs (82%) rename {kayak_core/src => src}/styles/corner.rs (96%) rename {kayak_core/src => src}/styles/edge.rs (95%) create mode 100644 src/styles/mod.rs create mode 100644 src/styles/options_ref.rs rename {kayak_core/src => src/styles}/render_command.rs (59%) rename kayak_core/src/styles/mod.rs => src/styles/style.rs (92%) rename {kayak_core/src => src}/tree.rs (77%) create mode 100644 src/widget.rs create mode 100644 src/widget_context.rs delete mode 100644 src/widgets/fold.rs delete mode 100644 src/widgets/if_element.rs delete mode 100644 src/widgets/inspector.rs delete mode 100644 src/widgets/on_change.rs delete mode 100644 src/widgets/spin_box.rs delete mode 100644 src/widgets/tooltip.rs rename bevy_kayak_renderer/src/lib.rs => src/window_size.rs (66%) diff --git a/.gitignore b/.gitignore index 5e3b73d..b6a3ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ msdfgen .DS_Store -Cargo.lock \ No newline at end of file +Cargo.lock diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2cb4d64 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,490 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'kayak_ui'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=kayak_ui" + ], + "filter": { + "name": "kayak_ui", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'tabs'", + "cargo": { + "args": [ + "build", + "--example=tabs", + "--package=kayak_ui" + ], + "filter": { + "name": "tabs", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'tabs'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=tabs", + "--package=kayak_ui" + ], + "filter": { + "name": "tabs", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'clipping'", + "cargo": { + "args": [ + "build", + "--example=clipping", + "--package=kayak_ui" + ], + "filter": { + "name": "clipping", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'clipping'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=clipping", + "--package=kayak_ui" + ], + "filter": { + "name": "clipping", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'context'", + "cargo": { + "args": [ + "build", + "--example=context", + "--package=kayak_ui" + ], + "filter": { + "name": "context", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'context'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=context", + "--package=kayak_ui" + ], + "filter": { + "name": "context", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'image'", + "cargo": { + "args": [ + "build", + "--example=image", + "--package=kayak_ui" + ], + "filter": { + "name": "image", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'image'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=image", + "--package=kayak_ui" + ], + "filter": { + "name": "image", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'nine_patch'", + "cargo": { + "args": [ + "build", + "--example=nine_patch", + "--package=kayak_ui" + ], + "filter": { + "name": "nine_patch", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'nine_patch'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=nine_patch", + "--package=kayak_ui" + ], + "filter": { + "name": "nine_patch", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'quads'", + "cargo": { + "args": [ + "build", + "--example=quads", + "--package=kayak_ui" + ], + "filter": { + "name": "quads", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'quads'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=quads", + "--package=kayak_ui" + ], + "filter": { + "name": "quads", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'scrolling'", + "cargo": { + "args": [ + "build", + "--example=scrolling", + "--package=kayak_ui" + ], + "filter": { + "name": "scrolling", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'scrolling'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=scrolling", + "--package=kayak_ui" + ], + "filter": { + "name": "scrolling", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'simple'", + "cargo": { + "args": [ + "build", + "--example=simple", + "--package=kayak_ui" + ], + "filter": { + "name": "simple", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'simple'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=simple", + "--package=kayak_ui" + ], + "filter": { + "name": "simple", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'simple_state'", + "cargo": { + "args": [ + "build", + "--example=simple_state", + "--package=kayak_ui" + ], + "filter": { + "name": "simple_state", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'simple_state'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=simple_state", + "--package=kayak_ui" + ], + "filter": { + "name": "simple_state", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'text'", + "cargo": { + "args": [ + "build", + "--example=text", + "--package=kayak_ui" + ], + "filter": { + "name": "text", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'text'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=text", + "--package=kayak_ui" + ], + "filter": { + "name": "text", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'texture_atlas'", + "cargo": { + "args": [ + "build", + "--example=texture_atlas", + "--package=kayak_ui" + ], + "filter": { + "name": "texture_atlas", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'texture_atlas'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=texture_atlas", + "--package=kayak_ui" + ], + "filter": { + "name": "texture_atlas", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'kayak_font'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=kayak_font" + ], + "filter": { + "name": "kayak_font", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug example 'bevy'", + "cargo": { + "args": [ + "build", + "--example=bevy", + "--package=kayak_font" + ], + "filter": { + "name": "bevy", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in example 'bevy'", + "cargo": { + "args": [ + "test", + "--no-run", + "--example=bevy", + "--package=kayak_font" + ], + "filter": { + "name": "bevy", + "kind": "example" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6acc380..394a145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,32 +5,26 @@ edition = "2021" resolver = "2" [workspace] -members = ["bevy_kayak_ui", "kayak_core", "kayak_render_macros", "kayak_font"] +members = ["kayak_ui_macros"] -[features] -default = ["bevy_renderer"] -bevy_renderer = [ - "bevy_kayak_ui", - "kayak_core/bevy_renderer", - "kayak_font/bevy_renderer", - "bevy", -] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bevy = { version = "0.8.0", optional = true, default-features = false } -bevy_kayak_ui = { path = "bevy_kayak_ui", optional = true } -kayak_core = { path = "kayak_core" } -kayak_font = { path = "kayak_font" } -kayak_render_macros = { path = "kayak_render_macros" } +bevy = { git = "https://github.com/bevyengine/bevy", rev="9423cb6a8d0c140e11364eb23c8feb7e576baa8c" } +bytemuck = "1.12" +dashmap = "5.4" +kayak_font = { path = "./kayak_font" } +morphorm = { git = "https://github.com/geom3trik/morphorm", rev = "1243152d4cebea46fd3e5098df26402c73acae91" } +kayak_ui_macros = { path = "./kayak_ui_macros" } +indexmap = "1.9" [dev-dependencies] -bevy = "0.8.0" -rand = "0.8.4" - -[[example]] -name = "todo" -path = "examples/todo/todo.rs" +fastrand = "1.8" [[example]] name = "tabs" path = "examples/tabs/tabs.rs" + +[[example]] +name = "todo" +path = "examples/todo/todo.rs" diff --git a/LICENSE b/LICENSE index e2dc07a..387b7ea 100644 --- a/LICENSE +++ b/LICENSE @@ -3,4 +3,4 @@ Kayak UI is dual-licensed under either * MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT) * Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) -at your option. \ No newline at end of file +at your option. diff --git a/bevy_kayak_renderer/Cargo.toml b/bevy_kayak_renderer/Cargo.toml deleted file mode 100644 index 9dccd36..0000000 --- a/bevy_kayak_renderer/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "bevy_kayak_renderer" -version = "0.0.1" -edition = "2021" - -[dependencies] -bytemuck = "1.7.2" -kayak_font = { path = "../kayak_font" } -serde = "1.0" -serde_json = "1.0" -serde_path_to_error = "0.1" - -[dependencies.bevy] -version = "0.8.0" -default-features = false -features = [ - "bevy_render", - "bevy_sprite", - "bevy_core_pipeline" -] diff --git a/bevy_kayak_ui/Cargo.toml b/bevy_kayak_ui/Cargo.toml deleted file mode 100644 index 29f12fd..0000000 --- a/bevy_kayak_ui/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "bevy_kayak_ui" -version = "0.0.1" -edition = "2021" - -[dependencies] -bytemuck = "1.7.2" -kayak_core = { path = "../kayak_core" } -kayak_font = { path = "../kayak_font" } -bevy_kayak_renderer = { path = "../bevy_kayak_renderer" } -serde = "1.0" -serde_json = "1.0" -serde_path_to_error = "0.1" -log = "0.4" - -[dependencies.bevy] -version = "0.8.0" -features = ["bevy_winit"] -default-features = false diff --git a/bevy_kayak_ui/src/bevy_context.rs b/bevy_kayak_ui/src/bevy_context.rs deleted file mode 100644 index 26c4bc3..0000000 --- a/bevy_kayak_ui/src/bevy_context.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use kayak_core::context::KayakContext; - -/// A wrapper around `KayakContext` to be used in Bevy integrations -/// -/// ``` -/// use bevy::prelude::*; -/// use bevy_kayak_ui::BevyContext; -/// -/// fn ui_system(context: Res<BevyContext>) { -/// // ... -/// } -/// ``` -pub struct BevyContext { - pub kayak_context: Arc<RwLock<KayakContext>>, -} - -impl BevyContext { - /// Create a new `BevyContext` - /// - /// This takes a function that will setup the `KayakContext` and its widget tree. - /// - /// ``` - /// use bevy::prelude::*; - /// use bevy_kayak_ui::BevyContext; - /// - /// fn setup_context(mut commands: Commands) { - /// let context = BevyContext::new(|context| { - /// render! { - /// <> - /// // ... - /// </> - /// } - /// }); - /// - /// commands.insert_resource(context); - /// } - /// ``` - pub fn new<F: Fn(&mut KayakContext)>(f: F) -> Self { - let kayak_context = Arc::new(RwLock::new(KayakContext::new())); - - if let Ok(mut kayak_context) = kayak_context.write() { - f(&mut kayak_context); - kayak_context.widget_manager.dirty(true); - } - - Self { kayak_context } - } - - /// Returns true if the cursor is currently over a valid widget - /// - /// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own. - /// This means widgets specified with `RenderCommand::Empty`, `RenderCommand::Layout`, or `RenderCommand::Clip` - /// do not meet the requirements to "contain" the cursor. - pub fn contains_cursor(&self) -> bool { - if let Ok(kayak_context) = self.kayak_context.read() { - kayak_context.contains_cursor() - } else { - false - } - } - - /// Returns true if the cursor may be needed by a widget or it's already in use by one - /// - /// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets - /// include buttons, sliders, and text boxes. - pub fn wants_cursor(&self) -> bool { - if let Ok(kayak_context) = self.kayak_context.read() { - kayak_context.wants_cursor() - } else { - false - } - } - - /// Returns true if the cursor is currently in use by a widget - /// - /// This is most often useful for checking drag events as it will still return true even if the drag continues outside - /// the widget bounds (as long as it started within it). - pub fn has_cursor(&self) -> bool { - if let Ok(kayak_context) = self.kayak_context.read() { - kayak_context.has_cursor() - } else { - false - } - } -} diff --git a/bevy_kayak_ui/src/cursor.rs b/bevy_kayak_ui/src/cursor.rs deleted file mode 100644 index c3915e3..0000000 --- a/bevy_kayak_ui/src/cursor.rs +++ /dev/null @@ -1,41 +0,0 @@ -use kayak_core::CursorIcon; - -pub fn convert_cursor_icon(cursor_icon: CursorIcon) -> bevy::prelude::CursorIcon { - match cursor_icon { - CursorIcon::Default => bevy::prelude::CursorIcon::Default, - CursorIcon::Crosshair => bevy::prelude::CursorIcon::Crosshair, - CursorIcon::Hand => bevy::prelude::CursorIcon::Hand, - CursorIcon::Arrow => bevy::prelude::CursorIcon::Arrow, - CursorIcon::Move => bevy::prelude::CursorIcon::Move, - CursorIcon::Text => bevy::prelude::CursorIcon::Text, - CursorIcon::Wait => bevy::prelude::CursorIcon::Wait, - CursorIcon::Help => bevy::prelude::CursorIcon::Help, - CursorIcon::Progress => bevy::prelude::CursorIcon::Progress, - CursorIcon::NotAllowed => bevy::prelude::CursorIcon::NotAllowed, - CursorIcon::ContextMenu => bevy::prelude::CursorIcon::ContextMenu, - CursorIcon::Cell => bevy::prelude::CursorIcon::Cell, - CursorIcon::VerticalText => bevy::prelude::CursorIcon::VerticalText, - CursorIcon::Alias => bevy::prelude::CursorIcon::Alias, - CursorIcon::Copy => bevy::prelude::CursorIcon::Copy, - CursorIcon::NoDrop => bevy::prelude::CursorIcon::NoDrop, - CursorIcon::Grab => bevy::prelude::CursorIcon::Grab, - CursorIcon::Grabbing => bevy::prelude::CursorIcon::Grabbing, - CursorIcon::AllScroll => bevy::prelude::CursorIcon::AllScroll, - CursorIcon::ZoomIn => bevy::prelude::CursorIcon::ZoomIn, - CursorIcon::ZoomOut => bevy::prelude::CursorIcon::ZoomOut, - CursorIcon::EResize => bevy::prelude::CursorIcon::EResize, - CursorIcon::NResize => bevy::prelude::CursorIcon::NResize, - CursorIcon::NeResize => bevy::prelude::CursorIcon::NeResize, - CursorIcon::NwResize => bevy::prelude::CursorIcon::NwResize, - CursorIcon::SResize => bevy::prelude::CursorIcon::SResize, - CursorIcon::SeResize => bevy::prelude::CursorIcon::SeResize, - CursorIcon::SwResize => bevy::prelude::CursorIcon::SwResize, - CursorIcon::WResize => bevy::prelude::CursorIcon::WResize, - CursorIcon::EwResize => bevy::prelude::CursorIcon::EwResize, - CursorIcon::NsResize => bevy::prelude::CursorIcon::NsResize, - CursorIcon::NeswResize => bevy::prelude::CursorIcon::NeswResize, - CursorIcon::NwseResize => bevy::prelude::CursorIcon::NwseResize, - CursorIcon::ColResize => bevy::prelude::CursorIcon::ColResize, - CursorIcon::RowResize => bevy::prelude::CursorIcon::RowResize, - } -} diff --git a/bevy_kayak_ui/src/key.rs b/bevy_kayak_ui/src/key.rs deleted file mode 100644 index 9da2ba4..0000000 --- a/bevy_kayak_ui/src/key.rs +++ /dev/null @@ -1,169 +0,0 @@ -use kayak_core::KeyCode; - -pub fn convert_virtual_key_code(virtual_key_code: bevy::prelude::KeyCode) -> KeyCode { - match virtual_key_code { - bevy::prelude::KeyCode::Key1 => KeyCode::Key1, - bevy::prelude::KeyCode::Key2 => KeyCode::Key2, - bevy::prelude::KeyCode::Key3 => KeyCode::Key3, - bevy::prelude::KeyCode::Key4 => KeyCode::Key4, - bevy::prelude::KeyCode::Key5 => KeyCode::Key5, - bevy::prelude::KeyCode::Key6 => KeyCode::Key6, - bevy::prelude::KeyCode::Key7 => KeyCode::Key7, - bevy::prelude::KeyCode::Key8 => KeyCode::Key8, - bevy::prelude::KeyCode::Key9 => KeyCode::Key9, - bevy::prelude::KeyCode::Key0 => KeyCode::Key0, - bevy::prelude::KeyCode::A => KeyCode::A, - bevy::prelude::KeyCode::B => KeyCode::B, - bevy::prelude::KeyCode::C => KeyCode::C, - bevy::prelude::KeyCode::D => KeyCode::D, - bevy::prelude::KeyCode::E => KeyCode::E, - bevy::prelude::KeyCode::F => KeyCode::F, - bevy::prelude::KeyCode::G => KeyCode::G, - bevy::prelude::KeyCode::H => KeyCode::H, - bevy::prelude::KeyCode::I => KeyCode::I, - bevy::prelude::KeyCode::J => KeyCode::J, - bevy::prelude::KeyCode::K => KeyCode::K, - bevy::prelude::KeyCode::L => KeyCode::L, - bevy::prelude::KeyCode::M => KeyCode::M, - bevy::prelude::KeyCode::N => KeyCode::N, - bevy::prelude::KeyCode::O => KeyCode::O, - bevy::prelude::KeyCode::P => KeyCode::P, - bevy::prelude::KeyCode::Q => KeyCode::Q, - bevy::prelude::KeyCode::R => KeyCode::R, - bevy::prelude::KeyCode::S => KeyCode::S, - bevy::prelude::KeyCode::T => KeyCode::T, - bevy::prelude::KeyCode::U => KeyCode::U, - bevy::prelude::KeyCode::V => KeyCode::V, - bevy::prelude::KeyCode::W => KeyCode::W, - bevy::prelude::KeyCode::X => KeyCode::X, - bevy::prelude::KeyCode::Y => KeyCode::Y, - bevy::prelude::KeyCode::Z => KeyCode::Z, - bevy::prelude::KeyCode::Escape => KeyCode::Escape, - bevy::prelude::KeyCode::F1 => KeyCode::F1, - bevy::prelude::KeyCode::F2 => KeyCode::F2, - bevy::prelude::KeyCode::F3 => KeyCode::F3, - bevy::prelude::KeyCode::F4 => KeyCode::F4, - bevy::prelude::KeyCode::F5 => KeyCode::F5, - bevy::prelude::KeyCode::F6 => KeyCode::F6, - bevy::prelude::KeyCode::F7 => KeyCode::F7, - bevy::prelude::KeyCode::F8 => KeyCode::F8, - bevy::prelude::KeyCode::F9 => KeyCode::F9, - bevy::prelude::KeyCode::F10 => KeyCode::F10, - bevy::prelude::KeyCode::F11 => KeyCode::F11, - bevy::prelude::KeyCode::F12 => KeyCode::F12, - bevy::prelude::KeyCode::F13 => KeyCode::F13, - bevy::prelude::KeyCode::F14 => KeyCode::F14, - bevy::prelude::KeyCode::F15 => KeyCode::F15, - bevy::prelude::KeyCode::F16 => KeyCode::F16, - bevy::prelude::KeyCode::F17 => KeyCode::F17, - bevy::prelude::KeyCode::F18 => KeyCode::F18, - bevy::prelude::KeyCode::F19 => KeyCode::F19, - bevy::prelude::KeyCode::F20 => KeyCode::F20, - bevy::prelude::KeyCode::F21 => KeyCode::F21, - bevy::prelude::KeyCode::F22 => KeyCode::F22, - bevy::prelude::KeyCode::F23 => KeyCode::F23, - bevy::prelude::KeyCode::F24 => KeyCode::F24, - bevy::prelude::KeyCode::Snapshot => KeyCode::Snapshot, - bevy::prelude::KeyCode::Scroll => KeyCode::Scroll, - bevy::prelude::KeyCode::Pause => KeyCode::Pause, - bevy::prelude::KeyCode::Insert => KeyCode::Insert, - bevy::prelude::KeyCode::Home => KeyCode::Home, - bevy::prelude::KeyCode::Delete => KeyCode::Delete, - bevy::prelude::KeyCode::End => KeyCode::End, - bevy::prelude::KeyCode::PageDown => KeyCode::PageDown, - bevy::prelude::KeyCode::PageUp => KeyCode::PageUp, - bevy::prelude::KeyCode::Left => KeyCode::Left, - bevy::prelude::KeyCode::Up => KeyCode::Up, - bevy::prelude::KeyCode::Right => KeyCode::Right, - bevy::prelude::KeyCode::Down => KeyCode::Down, - bevy::prelude::KeyCode::Back => KeyCode::Back, - bevy::prelude::KeyCode::Return => KeyCode::Return, - bevy::prelude::KeyCode::Space => KeyCode::Space, - bevy::prelude::KeyCode::Compose => KeyCode::Compose, - bevy::prelude::KeyCode::Caret => KeyCode::Caret, - bevy::prelude::KeyCode::Numlock => KeyCode::Numlock, - bevy::prelude::KeyCode::Numpad0 => KeyCode::Numpad0, - bevy::prelude::KeyCode::Numpad1 => KeyCode::Numpad1, - bevy::prelude::KeyCode::Numpad2 => KeyCode::Numpad2, - bevy::prelude::KeyCode::Numpad3 => KeyCode::Numpad3, - bevy::prelude::KeyCode::Numpad4 => KeyCode::Numpad4, - bevy::prelude::KeyCode::Numpad5 => KeyCode::Numpad5, - bevy::prelude::KeyCode::Numpad6 => KeyCode::Numpad6, - bevy::prelude::KeyCode::Numpad7 => KeyCode::Numpad7, - bevy::prelude::KeyCode::Numpad8 => KeyCode::Numpad8, - bevy::prelude::KeyCode::Numpad9 => KeyCode::Numpad9, - bevy::prelude::KeyCode::AbntC1 => KeyCode::AbntC1, - bevy::prelude::KeyCode::AbntC2 => KeyCode::AbntC2, - bevy::prelude::KeyCode::NumpadAdd => KeyCode::NumpadAdd, - bevy::prelude::KeyCode::Apostrophe => KeyCode::Apostrophe, - bevy::prelude::KeyCode::Apps => KeyCode::Apps, - bevy::prelude::KeyCode::Asterisk => KeyCode::Asterisk, - bevy::prelude::KeyCode::Plus => KeyCode::Plus, - bevy::prelude::KeyCode::At => KeyCode::At, - bevy::prelude::KeyCode::Ax => KeyCode::Ax, - bevy::prelude::KeyCode::Backslash => KeyCode::Backslash, - bevy::prelude::KeyCode::Calculator => KeyCode::Calculator, - bevy::prelude::KeyCode::Capital => KeyCode::Capital, - bevy::prelude::KeyCode::Colon => KeyCode::Colon, - bevy::prelude::KeyCode::Comma => KeyCode::Comma, - bevy::prelude::KeyCode::Convert => KeyCode::Convert, - bevy::prelude::KeyCode::NumpadDecimal => KeyCode::NumpadDecimal, - bevy::prelude::KeyCode::NumpadDivide => KeyCode::NumpadDivide, - bevy::prelude::KeyCode::Equals => KeyCode::Equals, - bevy::prelude::KeyCode::Grave => KeyCode::Grave, - bevy::prelude::KeyCode::Kana => KeyCode::Kana, - bevy::prelude::KeyCode::Kanji => KeyCode::Kanji, - bevy::prelude::KeyCode::LAlt => KeyCode::LAlt, - bevy::prelude::KeyCode::LBracket => KeyCode::LBracket, - bevy::prelude::KeyCode::LControl => KeyCode::LControl, - bevy::prelude::KeyCode::LShift => KeyCode::LShift, - bevy::prelude::KeyCode::LWin => KeyCode::LWin, - bevy::prelude::KeyCode::Mail => KeyCode::Mail, - bevy::prelude::KeyCode::MediaSelect => KeyCode::MediaSelect, - bevy::prelude::KeyCode::MediaStop => KeyCode::MediaStop, - bevy::prelude::KeyCode::Minus => KeyCode::Minus, - bevy::prelude::KeyCode::NumpadMultiply => KeyCode::NumpadMultiply, - bevy::prelude::KeyCode::Mute => KeyCode::Mute, - bevy::prelude::KeyCode::MyComputer => KeyCode::MyComputer, - bevy::prelude::KeyCode::NavigateForward => KeyCode::NavigateForward, - bevy::prelude::KeyCode::NavigateBackward => KeyCode::NavigateBackward, - bevy::prelude::KeyCode::NextTrack => KeyCode::NextTrack, - bevy::prelude::KeyCode::NoConvert => KeyCode::NoConvert, - bevy::prelude::KeyCode::NumpadComma => KeyCode::NumpadComma, - bevy::prelude::KeyCode::NumpadEnter => KeyCode::NumpadEnter, - bevy::prelude::KeyCode::NumpadEquals => KeyCode::NumpadEquals, - bevy::prelude::KeyCode::Oem102 => KeyCode::Oem102, - bevy::prelude::KeyCode::Period => KeyCode::Period, - bevy::prelude::KeyCode::PlayPause => KeyCode::PlayPause, - bevy::prelude::KeyCode::Power => KeyCode::Power, - bevy::prelude::KeyCode::PrevTrack => KeyCode::PrevTrack, - bevy::prelude::KeyCode::RAlt => KeyCode::RAlt, - bevy::prelude::KeyCode::RBracket => KeyCode::RBracket, - bevy::prelude::KeyCode::RControl => KeyCode::RControl, - bevy::prelude::KeyCode::RShift => KeyCode::RShift, - bevy::prelude::KeyCode::RWin => KeyCode::RWin, - bevy::prelude::KeyCode::Semicolon => KeyCode::Semicolon, - bevy::prelude::KeyCode::Slash => KeyCode::Slash, - bevy::prelude::KeyCode::Sleep => KeyCode::Sleep, - bevy::prelude::KeyCode::Stop => KeyCode::Stop, - bevy::prelude::KeyCode::NumpadSubtract => KeyCode::NumpadSubtract, - bevy::prelude::KeyCode::Sysrq => KeyCode::Sysrq, - bevy::prelude::KeyCode::Tab => KeyCode::Tab, - bevy::prelude::KeyCode::Underline => KeyCode::Underline, - bevy::prelude::KeyCode::Unlabeled => KeyCode::Unlabeled, - bevy::prelude::KeyCode::VolumeDown => KeyCode::VolumeDown, - bevy::prelude::KeyCode::VolumeUp => KeyCode::VolumeUp, - bevy::prelude::KeyCode::Wake => KeyCode::Wake, - bevy::prelude::KeyCode::WebBack => KeyCode::WebBack, - bevy::prelude::KeyCode::WebFavorites => KeyCode::WebFavorites, - bevy::prelude::KeyCode::WebForward => KeyCode::WebForward, - bevy::prelude::KeyCode::WebHome => KeyCode::WebHome, - bevy::prelude::KeyCode::WebRefresh => KeyCode::WebRefresh, - bevy::prelude::KeyCode::WebSearch => KeyCode::WebSearch, - bevy::prelude::KeyCode::WebStop => KeyCode::WebStop, - bevy::prelude::KeyCode::Yen => KeyCode::Yen, - bevy::prelude::KeyCode::Copy => KeyCode::Copy, - bevy::prelude::KeyCode::Paste => KeyCode::Paste, - bevy::prelude::KeyCode::Cut => KeyCode::Cut, - } -} diff --git a/bevy_kayak_ui/src/lib.rs b/bevy_kayak_ui/src/lib.rs deleted file mode 100644 index 9ea3eae..0000000 --- a/bevy_kayak_ui/src/lib.rs +++ /dev/null @@ -1,184 +0,0 @@ -use bevy::{ - input::{ - keyboard::KeyboardInput, - mouse::{MouseButtonInput, MouseScrollUnit, MouseWheel}, - ButtonState, - }, - math::Vec2, - prelude::{EventReader, IntoExclusiveSystem, MouseButton, Plugin, Res, World}, - render::color::Color, - window::{CursorMoved, ReceivedCharacter, WindowCreated, WindowResized, Windows}, -}; - -mod bevy_context; -mod cursor; -mod key; -mod render; - -use crate::cursor::convert_cursor_icon; -pub use bevy_context::BevyContext; -pub use bevy_kayak_renderer::camera::*; -use kayak_core::{bind, Binding, InputEvent, MutableBound}; -pub use render::font::FontMapping; -pub use render::image::ImageManager; - -#[derive(Default)] -pub struct BevyKayakUIPlugin; - -impl Plugin for BevyKayakUIPlugin { - fn build(&self, app: &mut bevy::prelude::App) { - app.insert_resource(bind(WindowSize::default())) - .add_plugin(bevy_kayak_renderer::BevyKayakRendererPlugin) - .add_plugin(render::BevyKayakUIExtractPlugin) - .add_system(update_window_size) - .add_system(process_events.exclusive_system()) - .add_system(update.exclusive_system()); - } -} - -pub(crate) fn to_bevy_color(color: &kayak_core::color::Color) -> Color { - Color::rgba(color.r, color.g, color.b, color.a) -} - -pub fn update(world: &mut World) { - if let Some(bevy_context) = world.remove_resource::<BevyContext>() { - if let Ok(mut context) = bevy_context.kayak_context.write() { - context.set_global(std::mem::take(world)); - context.render(); - *world = context.remove_global::<World>().unwrap(); - - if let Some(ref mut windows) = world.get_resource_mut::<Windows>() { - if let Some(window) = windows.get_primary_mut() { - window.set_cursor_icon(convert_cursor_icon(context.cursor_icon())); - } - } - } - - world.insert_resource(bevy_context); - } -} - -pub fn process_events(world: &mut World) { - let window_size = if let Some(windows) = world.get_resource::<Windows>() { - if let Some(window) = windows.get_primary() { - Vec2::new(window.width(), window.height()) - } else { - log::warn!("Couldn't find primiary window!"); - return; - } - } else { - log::warn!("Couldn't find primiary window!"); - return; - }; - - if let Some(bevy_context) = world.remove_resource::<BevyContext>() { - if let Ok(mut context) = bevy_context.kayak_context.write() { - let mut input_events = Vec::new(); - - context.set_global(std::mem::take(world)); - context.query_world::<( - EventReader<CursorMoved>, - EventReader<MouseButtonInput>, - EventReader<MouseWheel>, - EventReader<ReceivedCharacter>, - EventReader<KeyboardInput>, - ), _, _>( - |( - mut cursor_moved_events, - mut mouse_button_input_events, - mut mouse_wheel_events, - mut char_input_events, - mut keyboard_input_events, - )| { - if let Some(event) = cursor_moved_events.iter().last() { - // Currently, we can only handle a single MouseMoved event at a time so everything but the last needs to be skipped - input_events.push(InputEvent::MouseMoved(( - event.position.x as f32, - window_size.y - event.position.y as f32, - ))); - } - - for event in mouse_button_input_events.iter() { - match event.button { - MouseButton::Left => { - if event.state == ButtonState::Pressed { - input_events.push(InputEvent::MouseLeftPress); - } else if event.state == ButtonState::Released { - input_events.push(InputEvent::MouseLeftRelease); - } - } - _ => {} - } - } - - for MouseWheel { x, y, unit } in mouse_wheel_events.iter() { - input_events.push(InputEvent::Scroll { - dx: *x, - dy: *y, - is_line: matches!(unit, MouseScrollUnit::Line), - }) - } - - for event in char_input_events.iter() { - input_events.push(InputEvent::CharEvent { c: event.char }); - } - - for event in keyboard_input_events.iter() { - if let Some(key_code) = event.key_code { - let kayak_key_code = key::convert_virtual_key_code(key_code); - input_events.push(InputEvent::Keyboard { - key: kayak_key_code, - is_pressed: matches!(event.state, ButtonState::Pressed), - }); - } - } - }, - ); - - context.process_events(input_events); - *world = context.remove_global::<World>().unwrap() - } - - world.insert_resource(bevy_context); - } -} - -/// Tracks the bevy window size. -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct WindowSize(pub f32, pub f32); - -fn update_window_size( - mut window_resized_events: EventReader<WindowResized>, - mut window_created_events: EventReader<WindowCreated>, - windows: Res<Windows>, - window_size: Res<Binding<WindowSize>>, -) { - let mut changed_window_ids = Vec::new(); - // handle resize events. latest events are handled first because we only want to resize each - // window once - for event in window_resized_events.iter().rev() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } - - // handle resize events. latest events are handled first because we only want to resize each - // window once - for event in window_created_events.iter().rev() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } - - for window_id in changed_window_ids { - if let Some(window) = windows.get(window_id) { - let width = window.width(); - let height = window.height(); - window_size.set(WindowSize(width, height)); - } - } -} diff --git a/bevy_kayak_ui/src/render/image/image_manager.rs b/bevy_kayak_ui/src/render/image/image_manager.rs deleted file mode 100644 index 8e64620..0000000 --- a/bevy_kayak_ui/src/render/image/image_manager.rs +++ /dev/null @@ -1,72 +0,0 @@ -use bevy::{prelude::Handle, render::texture::Image, utils::HashMap}; - -/// A resource used to manage images for use in a `KayakContext` -/// -/// # Example -/// -/// ``` -/// use bevy::prelude::*; -/// use bevy_kayak_ui::ImageManager; -/// -/// fn setup_ui( -/// # mut commands: Commands, -/// asset_server: Res<AssetServer>, -/// mut image_manager: ResMut<ImageManager> -/// ) { -/// # commands.spawn_bundle(UICameraBundle::new()); -/// # -/// let handle: Handle<Image> = asset_server.load("some-image.png"); -/// let ui_image_handle = image_manager.get(&handle); -/// // ... -/// # -/// # let context = BevyContext::new(|context| { -/// # render! { -/// # <App> -/// # <Image handle={ui_image_handle} /> -/// # </App> -/// # } -/// # }); -/// # -/// # commands.insert_resource(context); -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct ImageManager { - /// The current number of tracked images (used for assigning IDs) - count: u16, - /// A map of image IDs to their _strong_ handle - mapping: HashMap<u16, Handle<Image>>, - /// A map of _weak_ image handles to their ID - reverse_mapping: HashMap<Handle<Image>, u16>, -} - -impl ImageManager { - pub fn new() -> Self { - Self { - count: 0, - mapping: HashMap::default(), - reverse_mapping: HashMap::default(), - } - } - - /// Get the ID for the given handle - /// - /// If no handle is found, a _strong_ clone is made and added to the current mapping. - /// The newly created ID is then returned. - pub fn get(&mut self, image_handle: &Handle<Image>) -> u16 { - if let Some(id) = self.reverse_mapping.get(image_handle) { - return *id; - } else { - let id = self.count; - self.count += 1; - self.mapping.insert(id, image_handle.clone()); - self.reverse_mapping.insert(image_handle.clone_weak(), id); - return id; - } - } - - /// Get the image handle for the given ID - pub fn get_handle(&self, id: &u16) -> Option<&Handle<Image>> { - self.mapping.get(id) - } -} diff --git a/bevy_kayak_ui/src/render/image/mod.rs b/bevy_kayak_ui/src/render/image/mod.rs deleted file mode 100644 index 65709ef..0000000 --- a/bevy_kayak_ui/src/render/image/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use bevy::prelude::Plugin; - -mod extract; -mod image_manager; -pub use extract::extract_images; -pub use image_manager::ImageManager; - -pub struct ImageRendererPlugin; - -impl Plugin for ImageRendererPlugin { - fn build(&self, app: &mut bevy::prelude::App) { - app.insert_resource(ImageManager::new()); - } -} diff --git a/examples/bevy_event.rs b/examples/bevy_event.rs deleted file mode 100644 index 5b8365f..0000000 --- a/examples/bevy_event.rs +++ /dev/null @@ -1,82 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, EventReader, EventWriter, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{Style, StyleProp, Units}, - widget, EventType, OnEvent, -}; -use kayak_ui::widgets::{App, Button, Text, Window}; - -pub struct MyEvent; - -#[widget] -fn EventWindow() { - let button_text_styles = Style { - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - let on_event = OnEvent::new(move |ctx, event| match event.event_type { - EventType::Click(..) => { - ctx.query_world::<EventWriter<MyEvent>, _, ()>(|mut writer| writer.send(MyEvent)); - } - _ => {} - }); - - rsx! { - <> - <Window draggable={true} position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Bevy Event Example".to_string()}> - <Button on_event={Some(on_event)}> - <Text styles={Some(button_text_styles)} line_height={Some(40.0)} size={24.0} content={"Send bevy event".to_string()}>{}</Text> - </Button> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <EventWindow /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn on_my_event(mut reader: EventReader<MyEvent>) { - for _ in reader.iter() { - println!("MyEvent detected"); - } -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_event::<MyEvent>() - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .add_system(on_my_event) - .run(); -} diff --git a/examples/bevy_state.rs b/examples/bevy_state.rs deleted file mode 100644 index 9ebd91a..0000000 --- a/examples/bevy_state.rs +++ /dev/null @@ -1,116 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut, State, SystemSet}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{render, rsx, widget, Event, EventType, KayakContextRef, KeyCode, OnEvent}; -use kayak_ui::widgets::{App, Text}; - -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -enum GameState { - MainMenu, - Options, - Play, -} - -fn swap(mut state: ResMut<State<GameState>>) { - if *state.current() == GameState::MainMenu { - let _ = state.set(GameState::Options); - } else if *state.current() == GameState::Options { - let _ = state.set(GameState::Play); - } else { - let _ = state.set(GameState::MainMenu); - } -} - -fn handle_input(context: &mut KayakContextRef, event: &mut Event) { - match event.event_type { - EventType::KeyDown(event) => { - if event.key() == KeyCode::Space { - context.query_world::<ResMut<State<GameState>>, _, _>(swap); - } - } - _ => {} - }; -} - -#[widget] -fn StateSwitcher() { - rsx! { - <Text content={"Press space to switch states!".to_string()} size={32.0} /> - } -} - -fn create_main_menu(mut commands: Commands) { - let context = BevyContext::new(|context| { - render! { - <App on_event={Some(OnEvent::new(handle_input))}> - <Text content={"Main Menu".to_string()} size={32.0} /> - <StateSwitcher /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn create_options_menu(mut commands: Commands) { - let context = BevyContext::new(|context| { - render! { - <App on_event={Some(OnEvent::new(handle_input))}> - <Text content={"Options".to_string()} size={32.0} /> - <StateSwitcher /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn create_play_menu(mut commands: Commands) { - let context = BevyContext::new(|context| { - render! { - <App on_event={Some(OnEvent::new(handle_input))}> - <Text content={"Play".to_string()} size={32.0} /> - <StateSwitcher /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); -} - -fn destroy(mut commands: Commands) { - commands.remove_resource::<BevyContext>(); -} -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_state(GameState::MainMenu) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .add_system_set(SystemSet::on_enter(GameState::MainMenu).with_system(create_main_menu)) - .add_system_set(SystemSet::on_exit(GameState::MainMenu).with_system(destroy)) - .add_system_set(SystemSet::on_enter(GameState::Options).with_system(create_options_menu)) - .add_system_set(SystemSet::on_exit(GameState::Options).with_system(destroy)) - .add_system_set(SystemSet::on_enter(GameState::Play).with_system(create_play_menu)) - .add_system_set(SystemSet::on_exit(GameState::Play).with_system(destroy)) - .run(); -} diff --git a/examples/clipping.rs b/examples/clipping.rs index 43887fc..d6e2ba6 100644 --- a/examples/clipping.rs +++ b/examples/clipping.rs @@ -1,79 +1,73 @@ use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut}, - window::WindowDescriptor, + prelude::{App, AssetServer, Commands, ImageSettings, Res, ResMut}, DefaultPlugins, }; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, - styles::{Edge, Style, StyleProp, Units}, -}; -use kayak_ui::widgets::{App, Clip, NinePatch, Text}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; fn startup( mut commands: Commands, - asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, ) { - commands.spawn_bundle(UICameraBundle::new()); - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - let handle: Handle<bevy::render::texture::Image> = asset_server.load("kenny/panel_brown.png"); - let panel_brown_handle = image_manager.get(&handle); + commands.spawn(UICameraBundle::new()); - let context = BevyContext::new(|context| { - let nine_patch_styles = Style { - width: StyleProp::Value(Units::Pixels(512.0)), - height: StyleProp::Value(Units::Pixels(512.0)), - offset: StyleProp::Value(Edge::all(Units::Stretch(1.0))), - padding: StyleProp::Value(Edge::all(Units::Pixels(25.0))), - ..Style::default() - }; + let image = asset_server.load("kenny/panel_brown.png"); - let clip_styles = Style { ..Style::default() }; + let mut widget_context = Context::new(); + let parent_id = None; - let lorem_ipsum = r#" -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed tellus neque. Proin tempus ligula a mi molestie aliquam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam venenatis consequat ultricies. Sed ac orci purus. Nullam velit nisl, dapibus vel mauris id, dignissim elementum sapien. Vestibulum faucibus sapien ut erat bibendum, id lobortis nisi luctus. Mauris feugiat at lectus at pretium. Pellentesque vitae finibus ante. Nulla non ex neque. Cras varius, lorem facilisis consequat blandit, lorem mauris mollis massa, eget consectetur magna sem vel enim. Nam aliquam risus pulvinar, volutpat leo eget, eleifend urna. Suspendisse in magna sed ligula vehicula volutpat non vitae augue. Phasellus aliquam viverra consequat. Nam rhoncus molestie purus, sed laoreet neque imperdiet eget. Sed egestas metus eget sodales congue. + let nine_patch_styles = KStyle { + width: StyleProp::Value(Units::Pixels(512.0)), + height: StyleProp::Value(Units::Pixels(512.0)), + offset: StyleProp::Value(Edge::all(Units::Stretch(1.0))), + padding: StyleProp::Value(Edge::all(Units::Pixels(25.0))), + ..KStyle::default() + }; -Sed vel ante placerat, posuere lacus sit amet, tempus enim. Cras ullamcorper ex vitae metus consequat, a blandit leo semper. Nunc lacinia porta massa, a tempus leo laoreet nec. Sed vel metus tincidunt, scelerisque ex sit amet, lacinia dui. In sollicitudin pulvinar odio vitae hendrerit. Maecenas mollis tempor egestas. Nulla facilisi. Praesent nisi turpis, accumsan eu lobortis vestibulum, ultrices id nibh. Suspendisse sed dui porta, mollis elit sed, ornare sem. Cras molestie est libero, quis faucibus leo semper at. - -Nulla vel nisl rutrum, fringilla elit non, mollis odio. Donec convallis arcu neque, eget venenatis sem mattis nec. Nulla facilisi. Phasellus risus elit, vehicula sit amet risus et, sodales ultrices est. Quisque vulputate felis orci, non tristique leo faucibus in. Duis quis velit urna. Sed rhoncus dolor vel commodo aliquet. In sed tempor quam. Nunc non tempus ipsum. Praesent mi lacus, vehicula eu dolor eu, condimentum venenatis diam. In tristique ligula a ligula dictum, eu dictum lacus consectetur. Proin elementum egestas pharetra. Nunc suscipit dui ac nisl maximus, id congue velit volutpat. Etiam condimentum, mauris ac sodales tristique, est augue accumsan elit, ut luctus est mi ut urna. Mauris commodo, tortor eget gravida lacinia, leo est imperdiet arcu, a ullamcorper dui sapien eget erat. - -Vivamus pulvinar dui et elit volutpat hendrerit. Praesent luctus dolor ut rutrum finibus. Fusce ut odio ultrices, laoreet est at, condimentum turpis. Morbi at ultricies nibh. Mauris tempus imperdiet porta. Proin sit amet tincidunt eros. Quisque rutrum lacus ac est vehicula dictum. Pellentesque nec augue mi. - -Vestibulum rutrum imperdiet nisl, et consequat massa porttitor vel. Ut velit justo, vehicula a nulla eu, auctor eleifend metus. Ut egestas malesuada metus, sit amet pretium nunc commodo ac. Pellentesque gravida, nisl in faucibus volutpat, libero turpis mattis orci, vitae tincidunt ligula ligula ut tortor. Maecenas vehicula lobortis odio in molestie. Curabitur dictum elit sed arcu dictum, ut semper nunc cursus. Donec semper felis non nisl tincidunt elementum. - "#.to_string(); - - render! { - <App> - <NinePatch - styles={Some(nine_patch_styles)} - border={Edge::all(30.0)} - handle={panel_brown_handle} - > - <Clip styles={Some(clip_styles)}> - <Text content={lorem_ipsum} size={14.0} /> - </Clip> - </NinePatch> - </App> - } - }); + let lorem_ipsum = r#" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed tellus neque. Proin tempus ligula a mi molestie aliquam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam venenatis consequat ultricies. Sed ac orci purus. Nullam velit nisl, dapibus vel mauris id, dignissim elementum sapien. Vestibulum faucibus sapien ut erat bibendum, id lobortis nisi luctus. Mauris feugiat at lectus at pretium. Pellentesque vitae finibus ante. Nulla non ex neque. Cras varius, lorem facilisis consequat blandit, lorem mauris mollis massa, eget consectetur magna sem vel enim. Nam aliquam risus pulvinar, volutpat leo eget, eleifend urna. Suspendisse in magna sed ligula vehicula volutpat non vitae augue. Phasellus aliquam viverra consequat. Nam rhoncus molestie purus, sed laoreet neque imperdiet eget. Sed egestas metus eget sodales congue. + + Sed vel ante placerat, posuere lacus sit amet, tempus enim. Cras ullamcorper ex vitae metus consequat, a blandit leo semper. Nunc lacinia porta massa, a tempus leo laoreet nec. Sed vel metus tincidunt, scelerisque ex sit amet, lacinia dui. In sollicitudin pulvinar odio vitae hendrerit. Maecenas mollis tempor egestas. Nulla facilisi. Praesent nisi turpis, accumsan eu lobortis vestibulum, ultrices id nibh. Suspendisse sed dui porta, mollis elit sed, ornare sem. Cras molestie est libero, quis faucibus leo semper at. + + Nulla vel nisl rutrum, fringilla elit non, mollis odio. Donec convallis arcu neque, eget venenatis sem mattis nec. Nulla facilisi. Phasellus risus elit, vehicula sit amet risus et, sodales ultrices est. Quisque vulputate felis orci, non tristique leo faucibus in. Duis quis velit urna. Sed rhoncus dolor vel commodo aliquet. In sed tempor quam. Nunc non tempus ipsum. Praesent mi lacus, vehicula eu dolor eu, condimentum venenatis diam. In tristique ligula a ligula dictum, eu dictum lacus consectetur. Proin elementum egestas pharetra. Nunc suscipit dui ac nisl maximus, id congue velit volutpat. Etiam condimentum, mauris ac sodales tristique, est augue accumsan elit, ut luctus est mi ut urna. Mauris commodo, tortor eget gravida lacinia, leo est imperdiet arcu, a ullamcorper dui sapien eget erat. + + Vivamus pulvinar dui et elit volutpat hendrerit. Praesent luctus dolor ut rutrum finibus. Fusce ut odio ultrices, laoreet est at, condimentum turpis. Morbi at ultricies nibh. Mauris tempus imperdiet porta. Proin sit amet tincidunt eros. Quisque rutrum lacus ac est vehicula dictum. Pellentesque nec augue mi. + + Vestibulum rutrum imperdiet nisl, et consequat massa porttitor vel. Ut velit justo, vehicula a nulla eu, auctor eleifend metus. Ut egestas malesuada metus, sit amet pretium nunc commodo ac. Pellentesque gravida, nisl in faucibus volutpat, libero turpis mattis orci, vitae tincidunt ligula ligula ut tortor. Maecenas vehicula lobortis odio in molestie. Curabitur dictum elit sed arcu dictum, ut semper nunc cursus. Donec semper felis non nisl tincidunt elementum. + "#.to_string(); - commands.insert_resource(context); + rsx! { + <KayakAppBundle> + <NinePatchBundle + nine_patch={NinePatch { + handle: image.clone(), + border: Edge::all(30.0), + }} + styles={nine_patch_styles} + > + <ClipBundle> + <TextWidgetBundle + text={TextProps { + content: lorem_ipsum, + size: 14.0, + ..Default::default() + }} + /> + </ClipBundle> + </NinePatchBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); } fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) + App::new() + .insert_resource(ImageSettings::default_nearest()) .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) .add_startup_system(startup) - .run(); + .run() } diff --git a/examples/context.rs b/examples/context.rs new file mode 100644 index 0000000..c37b684 --- /dev/null +++ b/examples/context.rs @@ -0,0 +1,391 @@ +//! This example demonstrates how to use the provider/consumer pattern for passing props down +//! to multiple descendants. +//! +//! The problem we'll be solving here is adding support for theming. +//! +//! One reason the provider/consumer pattern might be favored over a global state is that it allows +//! for better specificity and makes local contexts much easier to manage. In the case of theming, +//! this allows us to have multiple active themes, even if they are nested within each other! + +use bevy::{ + prelude::{ + App as BevyApp, AssetServer, Bundle, Changed, Color, Commands, Component, Entity, + ImageSettings, In, ParamSet, Query, Res, ResMut, Vec2, + }, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; + +/// The color theme struct we will be using across our demo widgets +#[derive(Component, Debug, Default, Clone, PartialEq)] +struct Theme { + name: String, + primary: Color, + secondary: Color, + background: Color, +} + +impl Theme { + fn vampire() -> Self { + Self { + name: "Vampire".to_string(), + primary: Color::rgba(1.0, 0.475, 0.776, 1.0), + secondary: Color::rgba(0.641, 0.476, 0.876, 1.0), + background: Color::rgba(0.157, 0.165, 0.212, 1.0), + } + } + fn solar() -> Self { + Self { + name: "Solar".to_string(), + primary: Color::rgba(0.514, 0.580, 0.588, 1.0), + secondary: Color::rgba(0.149, 0.545, 0.824, 1.0), + background: Color::rgba(0.026, 0.212, 0.259, 1.0), + } + } + fn vector() -> Self { + Self { + name: "Vector".to_string(), + primary: Color::rgba(0.533, 1.0, 0.533, 1.0), + secondary: Color::rgba(0.098, 0.451, 0.098, 1.0), + background: Color::rgba(0.004, 0.059, 0.004, 1.0), + } + } +} + +#[derive(Component, Debug, Default, Clone)] +struct ThemeButton { + pub theme: Theme, +} +impl Widget for ThemeButton {} + +#[derive(Bundle)] +pub struct ThemeButtonBundle { + theme_button: ThemeButton, + widget_name: WidgetName, +} + +impl Default for ThemeButtonBundle { + fn default() -> Self { + Self { + theme_button: Default::default(), + widget_name: ThemeButton::default().get_name(), + } + } +} + +fn update_theme_button( + In((widget_context, theme_button_entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query<&ThemeButton>, + changed_query: Query<&ThemeButton, Changed<ThemeButton>>, + mut context_query: ParamSet<(Query<&mut Theme>, Query<&Theme, Changed<Theme>>)>, +) -> bool { + if !context_query.p1().is_empty() || !changed_query.is_empty() { + if let Ok(theme_button) = query.get(theme_button_entity) { + if let Some(theme_context_entity) = + widget_context.get_context_entity::<Theme>(theme_button_entity) + { + if let Ok(theme) = context_query.p0().get_mut(theme_context_entity) { + let mut box_style = KStyle { + width: StyleProp::Value(Units::Pixels(30.0)), + height: StyleProp::Value(Units::Pixels(30.0)), + background_color: StyleProp::Value(theme_button.theme.primary), + ..Default::default() + }; + + if theme_button.theme.name == theme.name { + box_style.top = StyleProp::Value(Units::Pixels(3.0)); + box_style.left = StyleProp::Value(Units::Pixels(3.0)); + box_style.bottom = StyleProp::Value(Units::Pixels(3.0)); + box_style.right = StyleProp::Value(Units::Pixels(3.0)); + box_style.width = StyleProp::Value(Units::Pixels(24.0)); + box_style.height = StyleProp::Value(Units::Pixels(24.0)); + } + + let parent_id = Some(theme_button_entity); + rsx! { + <BackgroundBundle + styles={box_style} + on_event={OnEvent::new( + move |In((event_dispatcher_context, event, _entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + query: Query<&ThemeButton>, + mut context_query: Query<&mut Theme>, + | { + match event.event_type { + EventType::Click(..) => { + if let Ok(button) = query.get(theme_button_entity) { + if let Ok(mut context_theme) = context_query.get_mut(theme_context_entity) { + *context_theme = button.theme.clone(); + } + } + }, + _ => {} + } + (event_dispatcher_context, event) + }, + )} + /> + } + + return true; + } + } + } + } + + false +} + +#[derive(Component, Debug, Default, Clone)] +struct ThemeSelector; +impl Widget for ThemeSelector {} + +#[derive(Bundle)] +pub struct ThemeSelectorBundle { + theme_selector: ThemeSelector, + widget_name: WidgetName, +} + +impl Default for ThemeSelectorBundle { + fn default() -> Self { + Self { + theme_selector: Default::default(), + widget_name: ThemeSelector::default().get_name(), + } + } +} + +fn update_theme_selector( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query<&ThemeSelector, Changed<ThemeSelector>>, +) -> bool { + if let Ok(_) = query.get(entity) { + let button_container_style = KStyle { + layout_type: StyleProp::Value(LayoutType::Row), + width: StyleProp::Value(Units::Stretch(1.0)), + height: StyleProp::Value(Units::Auto), + top: StyleProp::Value(Units::Pixels(5.0)), + ..Default::default() + }; + + let vampire_theme = Theme::vampire(); + let solar_theme = Theme::solar(); + let vector_theme = Theme::vector(); + + let parent_id = Some(entity); + rsx! { + <ElementBundle styles={button_container_style}> + <ThemeButtonBundle theme_button={ThemeButton { theme: vampire_theme }} /> + <ThemeButtonBundle theme_button={ThemeButton { theme: solar_theme }} /> + <ThemeButtonBundle theme_button={ThemeButton { theme: vector_theme }} /> + </ElementBundle> + } + + return true; + } + + false +} + +#[derive(Component, Debug, Default, Clone)] +pub struct ThemeDemo { + is_root: bool, + context_entity: Option<Entity>, +} +impl Widget for ThemeDemo {} + +#[derive(Bundle)] +pub struct ThemeDemoBundle { + theme_demo: ThemeDemo, + widget_name: WidgetName, +} + +impl Default for ThemeDemoBundle { + fn default() -> Self { + Self { + theme_demo: Default::default(), + widget_name: ThemeDemo::default().get_name(), + } + } +} + +fn update_theme_demo( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query_set: ParamSet<(Query<&mut ThemeDemo>, Query<&ThemeDemo, Changed<ThemeDemo>>)>, + theme_context: Query<&Theme>, + detect_changes_query: Query<&Theme, Changed<Theme>>, +) -> bool { + if !detect_changes_query.is_empty() || !query_set.p1().is_empty() { + if let Ok(mut theme_demo) = query_set.p0().get_mut(entity) { + if let Some(theme_context_entity) = widget_context.get_context_entity::<Theme>(entity) { + if let Ok(theme) = theme_context.get(theme_context_entity) { + let select_lbl = if theme_demo.is_root { + format!("Select Theme (Current: {})", theme.name) + } else { + format!("Select A Different Theme (Current: {})", theme.name) + }; + + if theme_demo.is_root { + if theme_demo.context_entity.is_none() { + let theme_entity = commands.spawn(Theme::vector()).id(); + theme_demo.context_entity = Some(theme_entity); + } + } + + let context_entity = if let Some(entity) = theme_demo.context_entity { + entity + } else { + Entity::from_raw(1000000) + }; + let text_styles = KStyle { + color: StyleProp::Value(theme.primary), + height: StyleProp::Value(Units::Pixels(28.0)), + ..Default::default() + }; + let btn_style = KStyle { + background_color: StyleProp::Value(theme.secondary), + width: StyleProp::Value(Units::Stretch(0.75)), + height: StyleProp::Value(Units::Pixels(32.0)), + top: StyleProp::Value(Units::Pixels(5.0)), + left: StyleProp::Value(Units::Stretch(1.0)), + right: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }; + + let parent_id = Some(entity); + let mut children = kayak_ui::prelude::KChildren::new(); + rsx! { + <> + <TextWidgetBundle + text={TextProps { + content: select_lbl, + size: 14.0, + line_height: Some(28.0), + ..Default::default() + }} + styles={KStyle { + height: StyleProp::Value(Units::Pixels(28.0)), + ..Default::default() + }} + /> + <ThemeSelectorBundle /> + <BackgroundBundle + styles={KStyle { + background_color: StyleProp::Value(theme.background), + top: StyleProp::Value(Units::Pixels(15.0)), + width: StyleProp::Value(Units::Stretch(1.0)), + height: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }} + > + <TextWidgetBundle + text={TextProps { + content: "Lorem ipsum dolor...".into(), + size: 12.0, + ..Default::default() + }} + styles={text_styles.clone()} + /> + <KButtonBundle + styles={btn_style.clone()} + > + <TextWidgetBundle + text={TextProps { + content: "BUTTON".into(), + size: 14.0, + ..Default::default() + }} + /> + </KButtonBundle> + { + if theme_demo.is_root { + widget_context.set_context_entity::<Theme>( + parent_id, + context_entity, + ); + constructor! { + <ElementBundle + styles={KStyle { + top: StyleProp::Value(Units::Pixels(10.0)), + left: StyleProp::Value(Units::Pixels(10.0)), + bottom: StyleProp::Value(Units::Pixels(10.0)), + right: StyleProp::Value(Units::Pixels(10.0)), + ..Default::default() + }} + > + <ThemeDemoBundle /> + </ElementBundle> + } + } + } + </BackgroundBundle> + </> + } + + children.process(&widget_context, parent_id); + + return true; + } + } + } + } + + false +} + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + widget_context.add_widget_system(ThemeDemo::default().get_name(), update_theme_demo); + widget_context.add_widget_system(ThemeButton::default().get_name(), update_theme_button); + widget_context.add_widget_system(ThemeSelector::default().get_name(), update_theme_selector); + let parent_id = None; + rsx! { + <KayakAppBundle> + { + let theme_entity = commands.spawn(Theme::vampire()).id(); + widget_context.set_context_entity::<Theme>(parent_id, theme_entity); + } + <WindowBundle + window={KWindow { + title: "Context Example".into(), + draggable: true, + position: Vec2::ZERO, + size: Vec2::new(350.0, 400.0), + ..Default::default() + }} + > + <ThemeDemoBundle + theme_demo={ThemeDemo { + is_root: true, + context_entity: None, + }} + /> + </WindowBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn main() { + BevyApp::new() + .insert_resource(ImageSettings::default_nearest()) + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +} diff --git a/examples/counter.rs b/examples/counter.rs deleted file mode 100644 index 9b26433..0000000 --- a/examples/counter.rs +++ /dev/null @@ -1,82 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{Style, StyleProp, Units}, - use_state, widget, EventType, OnEvent, -}; -use kayak_ui::widgets::{App, Button, Text, Window}; - -#[widget] -fn Counter() { - let text_styles = Style { - bottom: StyleProp::Value(Units::Stretch(1.0)), - left: StyleProp::Value(Units::Stretch(0.1)), - right: StyleProp::Value(Units::Stretch(0.1)), - top: StyleProp::Value(Units::Stretch(1.0)), - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Pixels(28.0)), - ..Default::default() - }; - - let button_text_styles = Style { - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - let (count, set_count, ..) = use_state!(0i32); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => set_count(count + 1), - _ => {} - }); - - rsx! { - <> - <Window draggable={true} position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Counter Example".to_string()}> - <Text styles={Some(text_styles)} size={32.0} content={format!("Current Count: {}", count).to_string()}>{}</Text> - <Button on_event={Some(on_event)}> - <Text styles={Some(button_text_styles)} line_height={Some(40.0)} size={24.0} content={"Count!".to_string()}>{}</Text> - </Button> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <Counter /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/fold.rs b/examples/fold.rs deleted file mode 100644 index d22c517..0000000 --- a/examples/fold.rs +++ /dev/null @@ -1,170 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; - -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{ - render, rsx, - styles::{Style, StyleProp, Units}, - use_state, widget, Color, EventType, Handler, OnEvent, - }, - widgets::{App, Background, Button, Fold, If, Text, Window}, -}; - -#[widget] -fn FolderTree() { - let button_text_styles = Style { - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Pixels(22.0)), - ..Default::default() - }; - - let button_styles = Style { - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Pixels(24.0)), - background_color: StyleProp::Value(Color::new(0.33, 0.33, 0.33, 1.0)), - ..Default::default() - }; - - let fold_child_base_styles = Style { - left: StyleProp::Value(Units::Pixels(5.0)), - // Children need to be sized - height: StyleProp::Value(Units::Auto), - ..Default::default() - }; - - // === Folder A === // - let fold_a_styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.25882, 0.24314, 0.19608, 1.0)), - ..Default::default() - }); - let fold_a_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.16863, 0.16863, 0.12549, 1.0)), - ..fold_child_base_styles.clone() - }; - let fold_a_child_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.12941, 0.12941, 0.09412, 1.0)), - ..fold_a_child_styles.clone() - }; - - // === Folder B === // - let fold_b_styles = Style { - background_color: StyleProp::Value(Color::new(0.19608, 0.25882, 0.21569, 1.0)), - ..Default::default() - }; - let fold_b_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.11765, 0.16078, 0.12941, 1.0)), - ..fold_child_base_styles.clone() - }; - - let (is_b_open, set_b_open, ..) = use_state!(false); - let set_close_b = set_b_open.clone(); - let close_b = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => set_close_b(false), - _ => {} - })); - let set_open_b = set_b_open.clone(); - let open_b = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => set_open_b(true), - _ => {} - })); - - // === Folder C === // - let fold_c_styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.25882, 0.19608, 0.23529, 1.0)), - ..Default::default() - }); - let fold_c_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.16863, 0.12549, 0.15294, 1.0)), - ..fold_child_base_styles.clone() - }; - let try_style = Style { - color: StyleProp::Value(Color::new(1.0, 0.5, 0.5, 1.0)), - ..Style::default() - }; - - let (tried, set_tried, ..) = use_state!(false); - let on_toggle_c = Some(Handler::new(move |_| { - set_tried(true); - })); - - rsx! { - <> - <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Fold Example".to_string()}> - // === Folder A === // - <Fold label={"Folder A".to_string()} styles={fold_a_styles}> - <Fold label={"Folder A1".to_string()} default_open={true} styles={Some(fold_a_child_styles)}> - <Background styles={Some(fold_a_child_child_styles)}> - <Text size={12.0} content={"I default open".to_string()}>{}</Text> - </Background> - </Fold> - <Fold label={"Folder A2".to_string()} styles={Some(fold_a_child_styles)}> - <Background styles={Some(fold_a_child_child_styles)}> - <Text size={12.0} content={"I default closed".to_string()}>{}</Text> - </Background> - </Fold> - </Fold> - // === Folder B === // - <Fold label={"Folder B".to_string()} open={Some(is_b_open)} styles={Some(fold_b_styles)}> - <Background styles={Some(fold_b_child_styles)}> - <Text size={12.0} content={"The open/close state is manually controlled.".to_string()}>{}</Text> - <Text size={12.0} content={"Click the button to close:".to_string()}>{}</Text> - <Button on_event={close_b} styles={Some(button_styles)}> - <Text styles={Some(button_text_styles)} size={14.0} content={"Close B".to_string()}>{}</Text> - </Button> - </Background> - </Fold> - // === Folder C === // - <Fold label={"Folder C".to_string()} open={Some(true)} on_change={on_toggle_c} styles={fold_c_styles}> - <Background styles={Some(fold_c_child_styles)}> - <Text size={12.0} content={"Can't close me!".to_string()}>{}</Text> - <If condition={tried}> - <Text styles={Some(try_style)} size={12.0} content={"Nice try".to_string()}>{}</Text> - </If> - </Background> - </Fold> - - <Button on_event={open_b} styles={Some(button_styles)}> - <Text styles={Some(button_text_styles)} size={14.0} content={"Open B".to_string()}>{}</Text> - </Button> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <FolderTree /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/full_ui.rs b/examples/full_ui.rs deleted file mode 100644 index cc4188d..0000000 --- a/examples/full_ui.rs +++ /dev/null @@ -1,164 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut, World}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{Edge, LayoutType, Style, StyleProp, Units}, - widget, Bound, Children, EventType, MutableBound, OnEvent, WidgetProps, -}; -use kayak_ui::widgets::{App, NinePatch, Text}; - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -struct BlueButtonProps { - #[prop_field(Styles)] - styles: Option<Style>, - #[prop_field(Children)] - children: Option<Children>, -} - -#[widget] -fn BlueButton(props: BlueButtonProps) { - let (blue_button_handle, blue_button_hover_handle) = { - let world = context.get_global_mut::<World>(); - if world.is_err() { - return; - } - - let mut world = world.unwrap(); - - let (handle1, handle2) = { - let asset_server = world.get_resource::<AssetServer>().unwrap(); - let handle1: Handle<bevy::render::texture::Image> = - asset_server.load("../assets/kenny/buttonSquare_blue_pressed.png"); - let handle2: Handle<bevy::render::texture::Image> = - asset_server.load("../assets/kenny/buttonSquare_blue.png"); - - (handle1, handle2) - }; - - let mut image_manager = world.get_resource_mut::<ImageManager>().unwrap(); - let blue_button_handle = image_manager.get(&handle1); - let blue_button_hover_handle = image_manager.get(&handle2); - - (blue_button_handle, blue_button_hover_handle) - }; - - let current_button_handle = context.create_state::<u16>(blue_button_handle).unwrap(); - - let button_styles = Style { - width: StyleProp::Value(Units::Pixels(200.0)), - height: StyleProp::Value(Units::Pixels(50.0)), - padding: StyleProp::Value(Edge::all(Units::Stretch(1.0))), - ..props.styles.clone().unwrap_or_default() - }; - - let cloned_current_button_handle = current_button_handle.clone(); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::MouseIn(..) => { - cloned_current_button_handle.set(blue_button_hover_handle); - } - EventType::MouseOut(..) => { - cloned_current_button_handle.set(blue_button_handle); - } - _ => (), - }); - - let children = props.get_children(); - rsx! { - <NinePatch - border={Edge::all(10.0)} - handle={current_button_handle.get()} - styles={Some(button_styles)} - on_event={Some(on_event)} - > - {children} - </NinePatch> - } -} - -fn startup( - mut commands: Commands, - asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, - mut font_mapping: ResMut<FontMapping>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - let main_font = asset_server.load("antiquity.kayak_font"); - font_mapping.add("Antiquity", main_font.clone()); - - let handle: Handle<bevy::render::texture::Image> = asset_server.load("kenny/panel_brown.png"); - let panel_brown_handle = image_manager.get(&handle); - - let context = BevyContext::new(|context| { - let nine_patch_styles = Style { - layout_type: StyleProp::Value(LayoutType::Column), - width: StyleProp::Value(Units::Pixels(512.0)), - height: StyleProp::Value(Units::Pixels(512.0)), - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - top: StyleProp::Value(Units::Stretch(1.0)), - bottom: StyleProp::Value(Units::Stretch(1.0)), - padding: StyleProp::Value(Edge::all(Units::Stretch(1.0))), - ..Style::default() - }; - - let header_styles = Style { - bottom: StyleProp::Value(Units::Stretch(1.0)), - ..Style::default() - }; - - let options_button_styles = Style { - top: StyleProp::Value(Units::Pixels(15.0)), - ..Style::default() - }; - - let main_font_id = font_mapping.get(&main_font); - - render! { - <App> - <NinePatch - styles={Some(nine_patch_styles)} - border={Edge::all(30.0)} - handle={panel_brown_handle} - > - <Text - styles={Some(header_styles)} - size={35.0} - content={"Name My Game Plz".to_string()} - font={main_font_id} - /> - <BlueButton> - <Text line_height={Some(50.0)} size={20.0} content={"Play".to_string()} font={main_font_id} /> - </BlueButton> - <BlueButton styles={Some(options_button_styles)}> - <Text line_height={Some(50.0)} size={20.0} content={"Options".to_string()} font={main_font_id} /> - </BlueButton> - <BlueButton styles={Some(options_button_styles)}> - <Text line_height={Some(50.0)} size={20.0} content={"Quit".to_string()} font={main_font_id} /> - </BlueButton> - </NinePatch> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/global_counter.rs b/examples/global_counter.rs deleted file mode 100644 index a4d2053..0000000 --- a/examples/global_counter.rs +++ /dev/null @@ -1,69 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{bind, render, rsx, widget, Binding, Bound, MutableBound}; -use kayak_ui::widgets::{App, Text, Window}; - -#[derive(Clone, PartialEq)] -struct GlobalCount(pub u32); - -#[widget] -fn Counter() { - let global_count = context - .query_world::<Res<Binding<GlobalCount>>, _, _>(move |global_count| global_count.clone()); - - context.bind(&global_count); - - let global_count = global_count.get().0; - - rsx! { - <> - <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Counter Example".to_string()}> - <Text size={32.0} content={format!("Current Count: {}", global_count).to_string()}>{}</Text> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - commands.insert_resource(bind(GlobalCount(0))); - - let context = BevyContext::new(|context| { - render! { - <App> - <Counter /> - </App> - } - }); - commands.insert_resource(context); -} - -fn count_up(global_count: Res<Binding<GlobalCount>>) { - global_count.set(GlobalCount(global_count.get().0 + 1)); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .add_system(count_up) - .run(); -} diff --git a/examples/hooks.rs b/examples/hooks.rs deleted file mode 100644 index 170630f..0000000 --- a/examples/hooks.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! This example file demonstrates a few of the most common "hooks" used in this crate. For Kayak, a hook works much like -//! hooks in React: they hook into the lifecycle of their containing widget allowing deeper control over a widget's internal -//! logic. -//! -//! By convention, the macro "hooks" all start with the prefix `use_` (e.g., `use_state`). Another important thing to keep -//! in mind with these hooks are that they are just macros. They internally setup a lot of boilerplate code for you so that -//! you don't have to do it all manually. This means, though, that they may add variables or rely on external ones (many -//! hooks rely on the existence of a `KayakContext` instance named `context`, which is automatically inserted for every -//! widget, but could unintentionally be overwritten by a user-defined `context` variable). So be mindful of this when adding -//! these hooks to your widget— though issues regarding this should be fairly rare. -//! -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{render, rsx, use_effect, use_state, widget, EventType, OnEvent}, - widgets::{App, Button, Text, Window}, -}; - -/// A simple widget that tracks how many times a button is clicked using simple state data -#[widget] -fn StateCounter() { - // On its own, a widget can't track anything, since every value will just be reset when the widget is re-rendered. - // To get around this, and keep track of a value, we have to use states. States are values that are kept across renders. - // Additionally, anytime a state is updated with a new value, it causes the containing widget to re-render, making it - // useful for updating part of the UI with its value. - // To create a state, we can use the `use_state` macro. This creates a state with a given initial value, returning - // a tuple of its currently stored value and a closure for setting the stored value. - - // Here, we create a state with an initial value of 0. Right now the value of `count` is 0. If we call `set_count(10)`, - // then the new value of `count` will be 10. - let (count, set_count, ..) = use_state!(0); - - // We can create an event callback that uodates the state using the state variables defined above. - // Keep the borrow checker in mind! We can pass both `count` and `set_count` to this closure because they - // both implement `Copy`. For other types, you may have to clone the state to pass it into a closure like this. - // (You can also clone the setter as well if you need to use it in multiple places.) - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => set_count(count + 1), - _ => {} - }); - - rsx! { - <Window position={(50.0, 50.0)} size={(300.0, 150.0)} title={"State Example".to_string()}> - <Text size={16.0} content={format!("Current Count: {}", count)} /> - <Button on_event={Some(on_event)}> - <Text line_height={Some(40.0)} size={24.0} content={"Count!".to_string()} /> - </Button> - </Window> - } -} - -/// Another widget that tracks how many times a button is clicked using side-effects -#[widget] -fn EffectCounter() { - // In this widget, we're going to implement another counter, but this time using side-effects. - // To put it very simply, a side-effect is when something happens in response to something else happening. - // In our case, we want to create a side-effect that updates a counter when another state is updated. - - // In order to create this side-effect, we need access to the raw state binding. This is easily done by using - // the third field in the tuple returned from the `use_state` macro. - let (count, set_count, raw_count) = use_state!(0); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => set_count(count + 1), - _ => {} - }); - - // This is the state our side-effect will update in response to changes on `raw_count`. - let (effect_count, set_effect_count, ..) = use_state!(0); - - // This hook defines a side-effect that calls a function only when one of its dependencies is updated. - // They will also always run upon first render (i.e., when the widget is first added to the layout). - use_effect!( - move || { - // Update the `effect_count` state with the current `raw_count` value, multiplied by 2. - // Notice that we use `raw_count.get()` instead of `count`. This is because the closure is only defined once. - // This means that `count` will always be stuck at 0, as far as this hook is concerned. The solution is to - // use the `get` method on the raw state binding instead, to get the actual value. - set_effect_count(raw_count.get() * 2); - }, - // In order to call this side-effect closure whenever `raw_count` updates, we need to pass it in as a dependency. - // Don't worry about the borrow checker here, `raw_count` is automatically cloned internally, so you don't need - // to do that yourself. - [raw_count] // IMPORTANT: - // If a side-effect updates some other state, make sure you do not pass that state in as a dependency unless you have - // some checks in place to prevent an infinite loop! - ); - - // Passing an empty dependency array causes the callback to only run a single time: when the widget is first rendered. - use_effect!( - || { - println!("First!"); - }, - [] - ); - - // Additionally, order matters with these side-effects. They will be ran in the order they are defined. - use_effect!( - || { - println!("Second!"); - }, - [] - ); - - rsx! { - <Window position={(50.0, 225.0)} size={(300.0, 150.0)} title={"Effect Example".to_string()}> - <Text size={16.0} content={format!("Actual Count: {}", count)} /> - <Text size={16.0} content={format!("Doubled Count: {}", effect_count)} /> - <Button on_event={Some(on_event)}> - <Text line_height={Some(40.0)} size={24.0} content={"Count!".to_string()} /> - </Button> - </Window> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <StateCounter /> - <EffectCounter /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/if.rs b/examples/if.rs deleted file mode 100644 index 8719991..0000000 --- a/examples/if.rs +++ /dev/null @@ -1,80 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{Style, StyleProp, Units}, - widget, Bound, EventType, MutableBound, OnEvent, -}; -use kayak_ui::widgets::{App, Button, If, Text, Window}; - -#[widget] -fn Removal() { - let text_styles = Style { - bottom: StyleProp::Value(Units::Stretch(1.0)), - left: StyleProp::Value(Units::Stretch(0.1)), - right: StyleProp::Value(Units::Stretch(0.1)), - top: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - let is_visible = context.create_state(true).unwrap(); - let cloned_is_visible = is_visible.clone(); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - cloned_is_visible.set(!cloned_is_visible.get()); - } - _ => {} - }); - - let is_visible = is_visible.get(); - rsx! { - <> - <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"If Example".to_string()}> - <If condition={is_visible}> - <Text styles={Some(text_styles)} size={32.0} content={"Hello!".to_string()} /> - </If> - <Button on_event={Some(on_event)}> - <Text line_height={Some(40.0)} size={24.0} content={"Swap!".to_string()} /> - </Button> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <Removal /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/image.rs b/examples/image.rs index fb6d6cb..11e66a0 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,50 +1,44 @@ use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut}, - render::texture::ImageSettings, - window::WindowDescriptor, + prelude::{App as BevyApp, AssetServer, Commands, ImageSettings, Res, ResMut}, DefaultPlugins, }; -use kayak_core::styles::PositionType; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, - styles::{Corner, Style, StyleProp, Units}, -}; -use kayak_ui::widgets::{App, Image}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; fn startup( mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, ) { - commands.spawn_bundle(UICameraBundle::new()); - - let handle: Handle<bevy::render::texture::Image> = asset_server.load("generic-rpg-vendor.png"); - let ui_image_handle = image_manager.get(&handle); + font_mapping.set_default(asset_server.load("roboto.kayak_font")); - let context = BevyContext::new(|context| { - let image_styles = Style { - position_type: StyleProp::Value(PositionType::SelfDirected), - left: StyleProp::Value(Units::Pixels(10.0)), - top: StyleProp::Value(Units::Pixels(10.0)), - border_radius: StyleProp::Value(Corner::all(500.0)), - width: StyleProp::Value(Units::Pixels(200.0)), - height: StyleProp::Value(Units::Pixels(182.0)), - ..Style::default() - }; + commands.spawn(UICameraBundle::new()); - render! { - <App> - <Image styles={Some(image_styles)} handle={ui_image_handle} /> - </App> - } - }); + let image = asset_server.load("generic-rpg-vendor.png"); - commands.insert_resource(context); + let mut widget_context = Context::new(); + let parent_id = None; + rsx! { + <KayakAppBundle> + <ImageBundle + image={Image(image.clone())} + style={KStyle { + position_type: StyleProp::Value(PositionType::SelfDirected), + left: StyleProp::Value(Units::Pixels(10.0)), + top: StyleProp::Value(Units::Pixels(10.0)), + border_radius: StyleProp::Value(Corner::all(500.0)), + width: StyleProp::Value(Units::Pixels(200.0)), + height: StyleProp::Value(Units::Pixels(182.0)), + ..Default::default() + }} + /> + </KayakAppBundle> + } + commands.insert_resource(widget_context); } fn main() { BevyApp::new() +<<<<<<< HEAD .insert_resource(WindowDescriptor { width: 1270.0, height: 720.0, @@ -56,4 +50,12 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); +======= + .insert_resource(ImageSettings::default_nearest()) + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +>>>>>>> exp/main } diff --git a/examples/nine_patch.rs b/examples/nine_patch.rs index 640b555..c81088c 100644 --- a/examples/nine_patch.rs +++ b/examples/nine_patch.rs @@ -1,72 +1,66 @@ use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut}, - render::texture::ImageSettings, - window::WindowDescriptor, + prelude::{App as BevyApp, AssetServer, Commands, ImageSettings, Res, ResMut}, DefaultPlugins, }; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, - styles::{Edge, Style, StyleProp, Units}, -}; -use kayak_ui::widgets::{App, NinePatch}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; fn startup( mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, ) { - commands.spawn_bundle(UICameraBundle::new()); + font_mapping.set_default(asset_server.load("roboto.kayak_font")); - let handle: Handle<bevy::render::texture::Image> = asset_server.load("panel.png"); - let ui_image_handle = image_manager.get(&handle); + commands.spawn(UICameraBundle::new()); - let context = BevyContext::new(|context| { - // The border prop splits up the image into 9 quadrants like so: - // 1----2----3 - // | | - // 4 9 5 - // | | - // 6----7----8 - // The sizes of sprites for a 15 pixel border are as follows: - // TopLeft = (15, 15) - // TopRight = (15, 15) - // LeftCenter = (15, image_height) - // RightCenter = (15, image_height) - // TopCenter = (image_width, 15) - // BottomCenter = (image_width, 15) - // BottomRight = (15, 15) - // BottomLeft = (15, 15) - // Middle = ( - // 30 being left border + right border - // image_width - 30 - // 30 being top border + bottom border - // image_height - 30 - // ) - // + let image = asset_server.load("panel.png"); - let nine_patch_styles = Style { - width: StyleProp::Value(Units::Pixels(512.0)), - height: StyleProp::Value(Units::Pixels(512.0)), - ..Style::default() - }; + let mut widget_context = Context::new(); + let parent_id = None; - render! { - <App> - <NinePatch - styles={Some(nine_patch_styles)} - border={Edge::all(15.0)} - handle={ui_image_handle} - /> - </App> - } - }); + // The border prop splits up the image into 9 quadrants like so: + // 1----2----3 + // | | + // 4 9 5 + // | | + // 6----7----8 + // The sizes of sprites for a 15 pixel border are as follows: + // TopLeft = (15, 15) + // TopRight = (15, 15) + // LeftCenter = (15, image_height) + // RightCenter = (15, image_height) + // TopCenter = (image_width, 15) + // BottomCenter = (image_width, 15) + // BottomRight = (15, 15) + // BottomLeft = (15, 15) + // Middle = ( + // 30 being left border + right border + // image_width - 30 + // 30 being top border + bottom border + // image_height - 30 + // ) + rsx! { + <KayakAppBundle> + <NinePatchBundle + nine_patch={NinePatch { + handle: image.clone(), + border: Edge::all(15.0), + }} + styles={KStyle { + width: StyleProp::Value(Units::Pixels(512.0)), + height: StyleProp::Value(Units::Pixels(512.0)), + ..KStyle::default() + }} + /> + </KayakAppBundle> + } - commands.insert_resource(context); + commands.insert_resource(widget_context); } fn main() { BevyApp::new() +<<<<<<< HEAD .insert_resource(WindowDescriptor { width: 1270.0, height: 720.0, @@ -78,4 +72,12 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); +======= + .insert_resource(ImageSettings::default_nearest()) + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +>>>>>>> exp/main } diff --git a/examples/provider.rs b/examples/provider.rs deleted file mode 100644 index 349b632..0000000 --- a/examples/provider.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! This example demonstrates how to use the provider/consumer pattern for passing props down -//! to multiple descendants. -//! -//! The problem we'll be solving here is adding support for theming. Theming can generally be -//! added by using something like [set_global](kayak_core::KayakContext::set_global) -//! or [query_world](kayak_core::KayakContext::query_world). However, this example will demonstrate -//! an implementation using providers and consumers. -//! -//! One reason the provider/consumer pattern might be favored over a global state is that it allows -//! for better specificity and makes local contexts much easier to manage. In the case of theming, -//! this allows us to have multiple active themes, even if they are nested within each other! - -use bevy::prelude::{ - App as BevyApp, AssetServer, Commands, DefaultPlugins, Res, ResMut, WindowDescriptor, -}; -use kayak_core::Children; -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{ - render, rsx, - styles::{Edge, LayoutType, Style, StyleProp, Units}, - widget, Bound, Color, EventType, MutableBound, OnEvent, WidgetProps, - }, - widgets::{App, Background, Element, If, Text, TooltipConsumer, TooltipProvider, Window}, -}; -use std::sync::Arc; - -/// The color theme struct we will be using across our demo widgets -#[derive(Debug, Default, Clone, PartialEq)] -struct Theme { - name: String, - primary: Color, - secondary: Color, - background: Color, -} - -impl Theme { - fn vampire() -> Self { - Self { - name: "Vampire".to_string(), - primary: Color::new(1.0, 0.475, 0.776, 1.0), - secondary: Color::new(0.641, 0.476, 0.876, 1.0), - background: Color::new(0.157, 0.165, 0.212, 1.0), - } - } - fn solar() -> Self { - Self { - name: "Solar".to_string(), - primary: Color::new(0.514, 0.580, 0.588, 1.0), - secondary: Color::new(0.149, 0.545, 0.824, 1.0), - background: Color::new(0.026, 0.212, 0.259, 1.0), - } - } - fn vector() -> Self { - Self { - name: "Vector".to_string(), - primary: Color::new(0.533, 1.0, 0.533, 1.0), - secondary: Color::new(0.098, 0.451, 0.098, 1.0), - background: Color::new(0.004, 0.059, 0.004, 1.0), - } - } -} - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -struct ThemeProviderProps { - pub initial_theme: Theme, - #[prop_field(Children)] - children: Option<Children>, -} - -/// This widget provides a theme to its children -/// -/// Any descendant of this provider can access its theme by calling [create_consumer](kayak_core::KayakContext::create_consumer). -/// It can also be nested within itself, allowing for differing provider values. -#[widget] -fn ThemeProvider(props: ThemeProviderProps) { - let ThemeProviderProps { - initial_theme, - children, - } = props.clone(); - // Create the provider - context.create_provider(initial_theme); - rsx! { <>{children}</> } -} - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -struct ThemeButtonProps { - pub theme: Theme, -} - -/// A widget that shows a colored box, representing the theme -/// -/// This widget acts as one of our consumers of the [ThemeProvider]. It then uses the theme data to -/// display its content and also updates the shared state when clicked. -#[widget] -fn ThemeButton(props: ThemeButtonProps) { - let ThemeButtonProps { theme } = props.clone(); - // Create a consumer - // This grabs the current theme from the nearest ThemeProvider up the widget tree - let consumer = context - .create_consumer::<Theme>() - .expect("Requires ThemeProvider as an ancestor"); - - let theme_name = theme.name.clone(); - let consumer_theme_name = consumer.get().name.clone(); - let theme_primary = theme.primary.clone(); - - let theme_clone = Arc::new(theme); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - // Update the shared state - // This will cause the ThemeProvider to re-render along with all of the other consumers - consumer.set((*theme_clone).clone()); - } - _ => {} - }); - - let mut box_style = Style { - width: StyleProp::Value(Units::Pixels(30.0)), - height: StyleProp::Value(Units::Pixels(30.0)), - background_color: StyleProp::Value(theme_primary), - ..Default::default() - }; - - if theme_name == consumer_theme_name { - box_style.top = StyleProp::Value(Units::Pixels(3.0)); - box_style.left = StyleProp::Value(Units::Pixels(3.0)); - box_style.bottom = StyleProp::Value(Units::Pixels(3.0)); - box_style.right = StyleProp::Value(Units::Pixels(3.0)); - box_style.width = StyleProp::Value(Units::Pixels(24.0)); - box_style.height = StyleProp::Value(Units::Pixels(24.0)); - } - - rsx! { - <TooltipConsumer text={theme_name}> - <Background styles={Some(box_style)} on_event={Some(on_event)} /> - </TooltipConsumer> - } -} - -/// A widget displaying a set of [ThemeButton] widgets -/// -/// This is just an abstracted container. Not much to see here... -#[widget] -fn ThemeSelector() { - let vampire_theme = Theme::vampire(); - let solar_theme = Theme::solar(); - let vector_theme = Theme::vector(); - - let button_container_style = Style { - layout_type: StyleProp::Value(LayoutType::Row), - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Auto), - top: StyleProp::Value(Units::Pixels(5.0)), - ..Default::default() - }; - - rsx! { - <Element styles={Some(button_container_style)}> - <ThemeButton theme={vampire_theme} /> - <ThemeButton theme={solar_theme} /> - <ThemeButton theme={vector_theme} /> - </Element> - } -} - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -struct ThemeDemoProps { - is_root: bool, -} - -/// A widget that demonstrates the theming in action -/// -/// The `is_root` prop just ensures we don't recursively render this widget to infinity -#[widget] -fn ThemeDemo(props: ThemeDemoProps) { - // Create a consumer - // This grabs the current theme from the nearest ThemeProvider up the widget tree - let consumer = context - .create_consumer::<Theme>() - .expect("Requires ThemeProvider as an ancestor"); - let theme = consumer.get(); - - let select_lbl = if props.is_root { - format!("Select Theme (Current: {})", theme.name) - } else { - format!("Select A Different Theme (Current: {})", theme.name) - }; - - let select_lbl_style = Style { - height: StyleProp::Value(Units::Pixels(28.0)), - ..Default::default() - }; - - let bg_style = Style { - background_color: StyleProp::Value(theme.background), - top: StyleProp::Value(Units::Pixels(15.0)), - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - let text = "Lorem ipsum dolor...".to_string(); - let text_style = Style { - color: StyleProp::Value(theme.primary), - height: StyleProp::Value(Units::Pixels(28.0)), - ..Default::default() - }; - - let btn_text = "BUTTON".to_string(); - let btn_text_style = Style { - top: StyleProp::Value(Units::Pixels(4.0)), - ..Default::default() - }; - let btn_style = Style { - background_color: StyleProp::Value(theme.secondary), - width: StyleProp::Value(Units::Stretch(0.75)), - height: StyleProp::Value(Units::Pixels(32.0)), - top: StyleProp::Value(Units::Pixels(5.0)), - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - padding: StyleProp::Value(Edge::all(Units::Stretch(1.0))), - ..Default::default() - }; - - let nested_style = Style { - top: StyleProp::Value(Units::Pixels(10.0)), - left: StyleProp::Value(Units::Pixels(10.0)), - bottom: StyleProp::Value(Units::Pixels(10.0)), - right: StyleProp::Value(Units::Pixels(10.0)), - ..Default::default() - }; - - rsx! { - <> - <Text content={select_lbl} size={14.0} styles={Some(select_lbl_style)} /> - <ThemeSelector /> - - <Background styles={Some(bg_style)}> - - <Text content={text} size={12.0} styles={Some(text_style)} /> - <Background styles={Some(btn_style)}> - <Text content={btn_text} line_height={Some(20.0)} size={14.0} styles={Some(btn_text_style)} /> - </Background> - - <If condition={props.is_root}> - <Element styles={Some(nested_style)}> - - // This is one of the benefits of the provider/consumer pattern: - // We can nest a provider within the context of another provider. - // Doing this here allows us to apply alternate theming to the - // nested section without having to find it manually within a - // global state or resource. - <ThemeProvider initial_theme={Theme::vampire()}> - <ThemeDemo is_root={false} /> - </ThemeProvider> - - </Element> - </If> - - </Background> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <TooltipProvider size={(350.0, 350.0)} position={(0.0, 0.0)}> - <Window size={(350.0, 350.0)} position={(0.0, 0.0)} title={"Provider Example".to_string()}> - <ThemeProvider initial_theme={Theme::vampire()}> - <ThemeDemo is_root={true} /> - </ThemeProvider> - </Window> - </TooltipProvider> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/quads.rs b/examples/quads.rs new file mode 100644 index 0000000..7ad875f --- /dev/null +++ b/examples/quads.rs @@ -0,0 +1,106 @@ +use bevy::{ + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::{ + App as BevyApp, AssetServer, Bundle, Changed, Color, Commands, Component, Entity, In, + Query, Res, ResMut, Vec2, + }, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; +use morphorm::{PositionType, Units}; + +#[derive(Component, Default)] +pub struct MyQuad { + pos: Vec2, + pub size: Vec2, + pub color: Color, +} + +fn my_quad_update( + In((_widget_context, entity)): In<(WidgetContext, Entity)>, + mut query: Query<(&MyQuad, &mut KStyle), Changed<MyQuad>>, +) -> bool { + if let Ok((quad, mut style)) = query.get_mut(entity) { + style.render_command = StyleProp::Value(RenderCommand::Quad); + style.position_type = StyleProp::Value(PositionType::SelfDirected); + style.left = StyleProp::Value(Units::Pixels(quad.pos.x)); + style.top = StyleProp::Value(Units::Pixels(quad.pos.y)); + style.width = StyleProp::Value(Units::Pixels(quad.size.x)); + style.height = StyleProp::Value(Units::Pixels(quad.size.y)); + style.background_color = StyleProp::Value(quad.color); + return true; + } + + false +} + +impl Widget for MyQuad {} + +#[derive(Bundle)] +pub struct MyQuadBundle { + my_quad: MyQuad, + styles: KStyle, + widget_name: WidgetName, +} + +impl Default for MyQuadBundle { + fn default() -> Self { + Self { + my_quad: Default::default(), + styles: KStyle::default(), + widget_name: MyQuad::default().get_name(), + } + } +} + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + widget_context.add_widget_system(MyQuad::default().get_name(), my_quad_update); + let parent_id = None; + + rsx! { + <KayakAppBundle> + { + (0..1000).for_each(|_| { + let pos = Vec2::new(fastrand::i32(0..1280) as f32, fastrand::i32(0..720) as f32); + let size = Vec2::new( + fastrand::i32(32..64) as f32, + fastrand::i32(32..64) as f32, + ); + let color = Color::rgba( + fastrand::f32(), + fastrand::f32(), + fastrand::f32(), + 1.0, + ); + constructor! { + <MyQuadBundle + my_quad={MyQuad { pos, size, color }} + /> + } + }); + } + </KayakAppBundle> + } + + commands.insert_resource(widget_context); +} + +fn main() { + BevyApp::new() + .add_plugins(DefaultPlugins) + .add_plugin(LogDiagnosticsPlugin::default()) + .add_plugin(FrameTimeDiagnosticsPlugin::default()) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +} diff --git a/examples/render_target.rs b/examples/render_target.rs deleted file mode 100644 index 18abfc1..0000000 --- a/examples/render_target.rs +++ /dev/null @@ -1,157 +0,0 @@ -use bevy::{ - prelude::App as BevyApp, - prelude::*, - render::{ - camera::RenderTarget, - render_resource::{ - Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - }, - }, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{bind, render, rsx, widget, Binding, Bound, MutableBound}; -use kayak_ui::widgets::{App, Text, Window}; - -fn main() { - BevyApp::new() - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(setup) - .add_system(cube_rotator_system) - .add_system(count_up) - .run(); -} - -#[derive(Clone, PartialEq)] -struct GlobalCount(pub u32); - -#[widget] -fn Counter() { - let global_count = context - .query_world::<Res<Binding<GlobalCount>>, _, _>(move |global_count| global_count.clone()); - - context.bind(&global_count); - - let global_count = global_count.get().0; - - rsx! { - <> - <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Counter Example".to_string()}> - <Text size={32.0} content={format!("Current Count: {}", global_count).to_string()}>{}</Text> - </Window> - </> - } -} - -// Marks the main pass cube, to which the texture is applied. -#[derive(Component)] -struct MainPassCube; - -fn setup( - mut commands: Commands, - asset_server: Res<AssetServer>, - mut font_mapping: ResMut<FontMapping>, - mut meshes: ResMut<Assets<Mesh>>, - mut materials: ResMut<Assets<StandardMaterial>>, - mut images: ResMut<Assets<Image>>, -) { - let size = Extent3d { - width: 1280, - height: 720, - ..default() - }; - - // This is the texture that will be rendered to. - let mut image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::Bgra8UnormSrgb, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_DST - | TextureUsages::RENDER_ATTACHMENT, - }, - ..default() - }; - - // fill image.data with zeroes - image.resize(size); - - let image_handle = images.add(image); - - commands.spawn_bundle(UICameraBundle { - camera: Camera { - priority: -1, - target: RenderTarget::Image(image_handle.clone()), - ..Camera::default() - }, - ..UICameraBundle::new() - }); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - commands.insert_resource(bind(GlobalCount(0))); - - let context = BevyContext::new(|context| { - render! { - <App> - <Counter /> - </App> - } - }); - commands.insert_resource(context); - - // Light - // NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462 - commands.spawn_bundle(PointLightBundle { - transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), - ..default() - }); - - let cube_size = 4.0; - let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size))); - - // This material has the texture that has been rendered. - let material_handle = materials.add(StandardMaterial { - base_color_texture: Some(image_handle), - reflectance: 0.02, - unlit: false, - ..default() - }); - - // Main pass cube, with material containing the rendered first pass texture. - commands - .spawn_bundle(PbrBundle { - mesh: cube_handle, - material: material_handle, - transform: Transform { - translation: Vec3::new(0.0, 0.0, 1.5), - rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), - ..default() - }, - ..default() - }) - .insert(MainPassCube); - - // The main pass camera. - commands.spawn_bundle(Camera3dBundle { - transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) - .looking_at(Vec3::default(), Vec3::Y), - ..default() - }); -} - -/// Rotates the outer cube (main pass) -fn cube_rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<MainPassCube>>) { - for mut transform in &mut query { - transform.rotate_x(1.0 * time.delta_seconds()); - transform.rotate_y(0.7 * time.delta_seconds()); - } -} - -fn count_up(global_count: Res<Binding<GlobalCount>>) { - global_count.set(GlobalCount(global_count.get().0 + 1)); -} diff --git a/examples/scrollbox.rs b/examples/scrollbox.rs deleted file mode 100644 index fec1c35..0000000 --- a/examples/scrollbox.rs +++ /dev/null @@ -1,77 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, - styles::{Edge, Style, StyleProp, Units}, -}; -use kayak_ui::widgets::{App, Inspector, NinePatch, ScrollBox, Text}; - -fn startup( - mut commands: Commands, - asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, - mut font_mapping: ResMut<FontMapping>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let handle: Handle<bevy::render::texture::Image> = asset_server.load("kenny/panel_brown.png"); - let panel_brown_handle = image_manager.get(&handle); - - let context = BevyContext::new(|context| { - let nine_patch_styles = Style { - width: StyleProp::Value(Units::Pixels(512.0)), - height: StyleProp::Value(Units::Pixels(512.0)), - offset: StyleProp::Value(Edge::all(Units::Stretch(1.0))), - padding: StyleProp::Value(Edge::all(Units::Pixels(25.0))), - ..Style::default() - }; - - let lorem_ipsum = r#"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed tellus neque. Proin tempus ligula a mi molestie aliquam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam venenatis consequat ultricies. Sed ac orci purus. Nullam velit nisl, dapibus vel mauris id, dignissim elementum sapien. Vestibulum faucibus sapien ut erat bibendum, id lobortis nisi luctus. Mauris feugiat at lectus at pretium. Pellentesque vitae finibus ante. Nulla non ex neque. Cras varius, lorem facilisis consequat blandit, lorem mauris mollis massa, eget consectetur magna sem vel enim. Nam aliquam risus pulvinar, volutpat leo eget, eleifend urna. Suspendisse in magna sed ligula vehicula volutpat non vitae augue. Phasellus aliquam viverra consequat. Nam rhoncus molestie purus, sed laoreet neque imperdiet eget. Sed egestas metus eget sodales congue. - -Sed vel ante placerat, posuere lacus sit amet, tempus enim. Cras ullamcorper ex vitae metus consequat, a blandit leo semper. Nunc lacinia porta massa, a tempus leo laoreet nec. Sed vel metus tincidunt, scelerisque ex sit amet, lacinia dui. In sollicitudin pulvinar odio vitae hendrerit. Maecenas mollis tempor egestas. Nulla facilisi. Praesent nisi turpis, accumsan eu lobortis vestibulum, ultrices id nibh. Suspendisse sed dui porta, mollis elit sed, ornare sem. Cras molestie est libero, quis faucibus leo semper at. - -Nulla vel nisl rutrum, fringilla elit non, mollis odio. Donec convallis arcu neque, eget venenatis sem mattis nec. Nulla facilisi. Phasellus risus elit, vehicula sit amet risus et, sodales ultrices est. Quisque vulputate felis orci, non tristique leo faucibus in. Duis quis velit urna. Sed rhoncus dolor vel commodo aliquet. In sed tempor quam. Nunc non tempus ipsum. Praesent mi lacus, vehicula eu dolor eu, condimentum venenatis diam. In tristique ligula a ligula dictum, eu dictum lacus consectetur. Proin elementum egestas pharetra. Nunc suscipit dui ac nisl maximus, id congue velit volutpat. Etiam condimentum, mauris ac sodales tristique, est augue accumsan elit, ut luctus est mi ut urna. Mauris commodo, tortor eget gravida lacinia, leo est imperdiet arcu, a ullamcorper dui sapien eget erat. - -Vivamus pulvinar dui et elit volutpat hendrerit. Praesent luctus dolor ut rutrum finibus. Fusce ut odio ultrices, laoreet est at, condimentum turpis. Morbi at ultricies nibh. Mauris tempus imperdiet porta. Proin sit amet tincidunt eros. Quisque rutrum lacus ac est vehicula dictum. Pellentesque nec augue mi. - -Vestibulum rutrum imperdiet nisl, et consequat massa porttitor vel. Ut velit justo, vehicula a nulla eu, auctor eleifend metus. Ut egestas malesuada metus, sit amet pretium nunc commodo ac. Pellentesque gravida, nisl in faucibus volutpat, libero turpis mattis orci, vitae tincidunt ligula ligula ut tortor. Maecenas vehicula lobortis odio in molestie. Curabitur dictum elit sed arcu dictum, ut semper nunc cursus. Donec semper felis non nisl tincidunt elementum. - "#.to_string(); - - render! { - <App> - <NinePatch - styles={Some(nine_patch_styles)} - border={Edge::all(30.0)} - handle={panel_brown_handle} - > - <ScrollBox> - <Text content={lorem_ipsum} size={14.0} /> - </ScrollBox> - </NinePatch> - <Inspector/> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/scrolling.rs b/examples/scrolling.rs new file mode 100644 index 0000000..028845a --- /dev/null +++ b/examples/scrolling.rs @@ -0,0 +1,75 @@ +use bevy::{ + prelude::{App, AssetServer, Commands, ImageSettings, Res, ResMut}, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let image = asset_server.load("kenny/panel_brown.png"); + + let mut widget_context = Context::new(); + let parent_id = None; + + let nine_patch_styles = KStyle { + width: StyleProp::Value(Units::Pixels(512.0)), + height: StyleProp::Value(Units::Pixels(512.0)), + offset: StyleProp::Value(Edge::all(Units::Stretch(1.0))), + padding: StyleProp::Value(Edge::all(Units::Pixels(25.0))), + ..KStyle::default() + }; + + let lorem_ipsum = r#" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed tellus neque. Proin tempus ligula a mi molestie aliquam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam venenatis consequat ultricies. Sed ac orci purus. Nullam velit nisl, dapibus vel mauris id, dignissim elementum sapien. Vestibulum faucibus sapien ut erat bibendum, id lobortis nisi luctus. Mauris feugiat at lectus at pretium. Pellentesque vitae finibus ante. Nulla non ex neque. Cras varius, lorem facilisis consequat blandit, lorem mauris mollis massa, eget consectetur magna sem vel enim. Nam aliquam risus pulvinar, volutpat leo eget, eleifend urna. Suspendisse in magna sed ligula vehicula volutpat non vitae augue. Phasellus aliquam viverra consequat. Nam rhoncus molestie purus, sed laoreet neque imperdiet eget. Sed egestas metus eget sodales congue. + + Sed vel ante placerat, posuere lacus sit amet, tempus enim. Cras ullamcorper ex vitae metus consequat, a blandit leo semper. Nunc lacinia porta massa, a tempus leo laoreet nec. Sed vel metus tincidunt, scelerisque ex sit amet, lacinia dui. In sollicitudin pulvinar odio vitae hendrerit. Maecenas mollis tempor egestas. Nulla facilisi. Praesent nisi turpis, accumsan eu lobortis vestibulum, ultrices id nibh. Suspendisse sed dui porta, mollis elit sed, ornare sem. Cras molestie est libero, quis faucibus leo semper at. + + Nulla vel nisl rutrum, fringilla elit non, mollis odio. Donec convallis arcu neque, eget venenatis sem mattis nec. Nulla facilisi. Phasellus risus elit, vehicula sit amet risus et, sodales ultrices est. Quisque vulputate felis orci, non tristique leo faucibus in. Duis quis velit urna. Sed rhoncus dolor vel commodo aliquet. In sed tempor quam. Nunc non tempus ipsum. Praesent mi lacus, vehicula eu dolor eu, condimentum venenatis diam. In tristique ligula a ligula dictum, eu dictum lacus consectetur. Proin elementum egestas pharetra. Nunc suscipit dui ac nisl maximus, id congue velit volutpat. Etiam condimentum, mauris ac sodales tristique, est augue accumsan elit, ut luctus est mi ut urna. Mauris commodo, tortor eget gravida lacinia, leo est imperdiet arcu, a ullamcorper dui sapien eget erat. + + Vivamus pulvinar dui et elit volutpat hendrerit. Praesent luctus dolor ut rutrum finibus. Fusce ut odio ultrices, laoreet est at, condimentum turpis. Morbi at ultricies nibh. Mauris tempus imperdiet porta. Proin sit amet tincidunt eros. Quisque rutrum lacus ac est vehicula dictum. Pellentesque nec augue mi. + + Vestibulum rutrum imperdiet nisl, et consequat massa porttitor vel. Ut velit justo, vehicula a nulla eu, auctor eleifend metus. Ut egestas malesuada metus, sit amet pretium nunc commodo ac. Pellentesque gravida, nisl in faucibus volutpat, libero turpis mattis orci, vitae tincidunt ligula ligula ut tortor. Maecenas vehicula lobortis odio in molestie. Curabitur dictum elit sed arcu dictum, ut semper nunc cursus. Donec semper felis non nisl tincidunt elementum. + "#.to_string(); + + rsx! { + <KayakAppBundle> + <NinePatchBundle + nine_patch={NinePatch { + handle: image.clone(), + border: Edge::all(30.0), + }} + styles={nine_patch_styles} + > + <ScrollContextProviderBundle> + <ScrollBoxBundle> + <TextWidgetBundle + text={TextProps { + content: lorem_ipsum, + size: 14.0, + ..Default::default() + }} + /> + </ScrollBoxBundle> + </ScrollContextProviderBundle> + </NinePatchBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn main() { + App::new() + .insert_resource(ImageSettings::default_nearest()) + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +} diff --git a/examples/shrink_grow_layout.rs b/examples/shrink_grow_layout.rs deleted file mode 100644 index 1cd1912..0000000 --- a/examples/shrink_grow_layout.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! This example demonstrates how to use a [on_layout](kayak_core::WidgetProps::get_on_layout) -//! event in widgets. -//! -//! The problem here is strictly contrived for example purposes. -//! We use grow/shrink buttons to set the value of a `width` bound to an [crate::Background] element's width -//! On change of layout we print current width of that element and update the text of Width label. -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_core::{ - styles::{Edge, LayoutType, Style, StyleProp, Units}, - OnLayout, -}; -use kayak_core::{Color, EventType, OnEvent}; -use kayak_render_macros::use_state; -use kayak_ui::widgets::{App, Element, Text, Window}; -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - widgets::Button, -}; -use kayak_ui::{ - core::{render, rsx, widget}, - widgets::Background, -}; - -/// This widget provides a theme to its children -#[widget] -fn GrowShrink() { - // This is width of background element we update via buttons - let (background_width, set_width, _) = use_state!(150.0); - - let panel_style = Style { - layout_type: StyleProp::Value(LayoutType::Row), - width: StyleProp::Value(Units::Auto), - height: StyleProp::Value(Units::Pixels(50.0)), - offset: StyleProp::Value(Edge::all(Units::Pixels(10.0))), - ..Default::default() - }; - - // Grow/Shrink button styles - let button_styles = Style { - width: StyleProp::Value(Units::Pixels(100.0)), - height: StyleProp::Value(Units::Pixels(30.0)), - background_color: StyleProp::Value(Color::new(0.33, 0.33, 0.33, 1.0)), - offset: StyleProp::Value(Edge::all(Units::Pixels(10.0))), - ..Default::default() - }; - - // The background style of element growing/shrink - let fill = Style { - width: StyleProp::Value(Units::Pixels(background_width)), - height: StyleProp::Value(Units::Pixels(28.0)), - layout_type: StyleProp::Value(LayoutType::Column), - background_color: StyleProp::Value(Color::new(1.0, 0.0, 0.0, 1.0)), - ..Default::default() - }; - - // Cloned function for use in closures - let grow_fn = set_width.clone(); - let shrink_fn = set_width.clone(); - - let grow = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => grow_fn(background_width + rand::random::<f32>() * 10.0), - _ => {} - }); - - let shrink = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => shrink_fn(background_width - rand::random::<f32>() * 10.0), - _ => {} - }); - - // layout width will be used by width label which we update `on_layout` - let (layout_width, set_layout_width, _) = use_state!(0.0); - - let update_text = OnLayout::new(move |_, layout_event| { - println!("Layout changed! New width = {}", layout_event.layout.width); - set_layout_width(layout_event.layout.width); - }); - - rsx! { - <> - <Window position={(100.0, 100.0)} size={(400.0, 400.0)} title={"Grow/Shrink Example".to_string()}> - <Text size={25.0} content={format!("Width: {:?}", layout_width).to_string()} /> - <Element styles={Some(panel_style)}> - <Button styles={Some(button_styles)} on_event={Some(grow)}> - <Text size={20.0} content={"Grow".to_string()}/> - </Button> - <Button styles={Some(button_styles)} on_event={Some(shrink)}> - <Text size={20.0} content={"Shrink".to_string()}/> - </Button> - </Element> - <Background styles={Some(fill)} on_layout={Some(update_text)} /> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <GrowShrink /> - </App> - } - }); - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..c3f65f8 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,86 @@ +use bevy::prelude::*; +use kayak_ui::prelude::*; + +#[derive(Component, Default)] +pub struct MyWidget2 { + bar: u32, +} + +fn my_widget_2_update( + In((_widget_context, entity)): In<(WidgetContext, Entity)>, + query: Query<&MyWidget2, Or<(Added<MyWidget2>, Changed<MyWidget2>)>>, +) -> bool { + if let Ok(my_widget2) = query.get(entity) { + dbg!(my_widget2.bar); + } + + true +} + +impl Widget for MyWidget2 {} + +#[derive(Component, Default)] +pub struct MyWidget { + pub foo: u32, +} + +fn my_widget_1_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + my_resource: Res<MyResource>, + mut query: Query<&mut MyWidget>, +) -> bool { + if my_resource.is_changed() { + if let Ok(mut my_widget) = query.get_mut(entity) { + my_widget.foo = my_resource.0; + dbg!(my_widget.foo); + + let my_child = MyWidget2 { bar: my_widget.foo }; + let should_update = my_widget.foo == my_child.bar; + let child_id = commands + .spawn((my_child, MyWidget2::default().get_name())) + .id(); + widget_context.add_widget(Some(entity), child_id); + + return should_update; + } + } + + false +} + +impl Widget for MyWidget {} + +#[derive(Resource)] +pub struct MyResource(pub u32); + +fn startup(mut commands: Commands) { + let mut context = Context::new(); + context.add_widget_system(MyWidget::default().get_name(), my_widget_1_update); + context.add_widget_system(MyWidget2::default().get_name(), my_widget_2_update); + let entity = commands + .spawn(( + MyWidget { foo: 0 }, + kayak_ui::prelude::KStyle::default(), + MyWidget::default().get_name(), + )) + .id(); + context.add_widget(None, entity); + commands.insert_resource(context); +} + +fn update_resource(keyboard_input: Res<Input<KeyCode>>, mut my_resource: ResMut<MyResource>) { + if keyboard_input.just_pressed(KeyCode::Space) { + my_resource.0 += 1; + } +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .insert_resource(MyResource(1)) + .add_startup_system(startup) + .add_system(update_resource) + .run() +} diff --git a/examples/simple_state.rs b/examples/simple_state.rs new file mode 100644 index 0000000..8d9422c --- /dev/null +++ b/examples/simple_state.rs @@ -0,0 +1,122 @@ +use bevy::{ + prelude::{ + App as BevyApp, AssetServer, Bundle, Changed, Commands, Component, Entity, In, Query, Res, + ResMut, Vec2, + }, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, *}; + +#[derive(Component, Default)] +struct CurrentCount(pub u32); + +impl Widget for CurrentCount {} + +#[derive(Bundle)] +struct CurrentCountBundle { + count: CurrentCount, + styles: KStyle, + widget_name: WidgetName, +} + +impl Default for CurrentCountBundle { + fn default() -> Self { + Self { + count: CurrentCount::default(), + styles: KStyle::default(), + widget_name: CurrentCount::default().get_name(), + } + } +} + +fn current_count_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query<&CurrentCount, Changed<CurrentCount>>, +) -> bool { + if let Ok(current_count) = query.get(entity) { + let parent_id = Some(entity); + rsx! { + <TextWidgetBundle + text={ + TextProps { + content: format!("Current Count: {}", current_count.0).into(), + size: 16.0, + line_height: Some(40.0), + ..Default::default() + } + } + /> + } + + return true; + } + + false +} + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + let parent_id = None; + widget_context.add_widget_system(CurrentCount::default().get_name(), current_count_update); + rsx! { + <KayakAppBundle> + <WindowBundle + window={KWindow { + title: "State Example Window".into(), + draggable: true, + position: Vec2::new(10.0, 10.0), + size: Vec2::new(300.0, 250.0), + ..KWindow::default() + }} + > + <CurrentCountBundle id={"current_count_entity"} /> + <KButtonBundle + on_event={OnEvent::new( + move |In((event_dispatcher_context, event, _entity)): In<(EventDispatcherContext, Event, Entity)>, + mut query: Query<&mut CurrentCount>| { + match event.event_type { + EventType::Click(..) => { + if let Ok(mut current_count) = + query.get_mut(current_count_entity) + { + current_count.0 += 1; + } + } + _ => {} + } + (event_dispatcher_context, event) + }, + )} + > + <TextWidgetBundle + text={TextProps { + content: "Click me!".into(), + size: 16.0, + alignment: Alignment::Start, + ..Default::default() + }} + /> + </KButtonBundle> + </WindowBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn main() { + BevyApp::new() + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +} diff --git a/examples/tabs/tab.rs b/examples/tabs/tab.rs index 1e0eed4..314138e 100644 --- a/examples/tabs/tab.rs +++ b/examples/tabs/tab.rs @@ -1,121 +1,67 @@ -use kayak_ui::{ - core::{ - render_command::RenderCommand, - rsx, - styles::{Edge, LayoutType, Style, StyleProp, Units}, - use_state, widget, Bound, EventType, OnEvent, WidgetProps, - }, - widgets::{Background, Text}, +use bevy::prelude::{ + Bundle, ChangeTrackers, Changed, Color, Commands, Component, Entity, In, ParamSet, Query, }; +use kayak_ui::prelude::{ + widgets::BackgroundBundle, Edge, KChildren, KStyle, StyleProp, Units, Widget, WidgetContext, + WidgetName, +}; +use kayak_ui_macros::rsx; -use crate::TabTheme; +use crate::tab_context::TabContext; -#[derive(Clone, PartialEq)] -enum TabHoverState { - None, - Inactive, - Active, +#[derive(Component, Default)] +pub struct Tab { + pub index: usize, } -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TabProps { - pub content: String, - pub selected: bool, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, -} +impl Widget for Tab {} -/// The actual tab, displayed in a [TabBar](crate::tab_bar::TabBar) -#[widget] -pub fn Tab(props: TabProps) { - let TabProps { - content, selected, .. - } = props.clone(); +#[derive(Bundle)] +pub struct TabBundle { + pub tab: Tab, + pub children: KChildren, + pub widget_name: WidgetName, +} - let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); - let (focus_state, set_focus_state, ..) = use_state!(false); - let (hover_state, set_hover_state, ..) = use_state!(TabHoverState::None); - match hover_state { - TabHoverState::Inactive if selected => set_hover_state(TabHoverState::Active), - TabHoverState::Active if !selected => set_hover_state(TabHoverState::Inactive), - _ => {} - }; +impl Default for TabBundle { + fn default() -> Self { + Self { + tab: Default::default(), + children: Default::default(), + widget_name: Tab::default().get_name(), + } + } +} - let event_handler = OnEvent::new(move |_, event| match event.event_type { - EventType::Hover(..) => { - if selected { - set_hover_state(TabHoverState::Active); - } else { - set_hover_state(TabHoverState::Inactive); +pub fn tab_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: Query<(&KChildren, &mut Tab)>, + mut tab_context_query: ParamSet<( + Query<ChangeTrackers<TabContext>>, + Query<&mut TabContext, Changed<TabContext>>, + )>, +) -> bool { + if !tab_context_query.p1().is_empty() { + if let Ok((children, tab)) = query.get_mut(entity) { + let context_entity = widget_context + .get_context_entity::<TabContext>(entity) + .unwrap(); + if let Ok(tab_context) = tab_context_query.p1().get(context_entity) { + let parent_id = Some(entity); + let styles = KStyle { + background_color: StyleProp::Value(Color::rgba(0.0781, 0.0898, 0.101, 1.0)), + padding: StyleProp::Value(Edge::all(Units::Pixels(5.0))), + ..Default::default() + }; + if tab_context.current_index == tab.index { + rsx! { + <BackgroundBundle styles={styles} children={children.clone()} /> + } + } + return true; } } - EventType::MouseOut(..) => { - set_hover_state(TabHoverState::None); - } - EventType::Focus => { - set_focus_state(true); - } - EventType::Blur => { - set_focus_state(false); - } - _ => {} - }); - - let tab_color = match hover_state { - TabHoverState::None if selected => theme.get().active_tab.normal, - TabHoverState::None => theme.get().inactive_tab.normal, - TabHoverState::Inactive => theme.get().inactive_tab.hovered, - TabHoverState::Active => theme.get().active_tab.hovered, - }; - - let pad_x = Units::Pixels(2.0); - let bg_styles = Style { - background_color: StyleProp::Value(tab_color), - layout_type: StyleProp::Value(LayoutType::Row), - padding_left: StyleProp::Value(pad_x), - padding_right: StyleProp::Value(pad_x), - ..Default::default() - }; - - let border_width = Units::Pixels(2.0); - let border_styles = Style { - background_color: if focus_state { - StyleProp::Value(theme.get().focus) - } else { - StyleProp::Value(tab_color) - }, - padding: StyleProp::Value(Edge::all(border_width)), - layout_type: StyleProp::Value(LayoutType::Row), - ..Default::default() - }; - - let text_styles = Style { - background_color: if focus_state { - StyleProp::Value(theme.get().focus) - } else { - StyleProp::Value(tab_color) - }, - color: StyleProp::Value(theme.get().text.normal), - top: StyleProp::Value(Units::Stretch(0.1)), - bottom: StyleProp::Value(Units::Stretch(1.0)), - width: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - height: StyleProp::Value(Units::Pixels(theme.get().tab_height)), - max_width: StyleProp::Value(Units::Pixels(100.0)), - ..props.styles.clone().unwrap_or_default() - }); - - rsx! { - <Background focusable={Some(true)} on_event={Some(event_handler)} styles={Some(border_styles)}> - <Background styles={Some(bg_styles)}> - <Text content={content} size={12.0} styles={Some(text_styles)} /> - </Background> - </Background> } + false } diff --git a/examples/tabs/tab_bar.rs b/examples/tabs/tab_bar.rs deleted file mode 100644 index 083e415..0000000 --- a/examples/tabs/tab_bar.rs +++ /dev/null @@ -1,68 +0,0 @@ -use kayak_ui::{ - core::{ - constructor, rsx, - styles::{LayoutType, Style, StyleProp, Units}, - widget, Bound, EventType, Handler, KeyCode, OnEvent, VecTracker, WidgetProps, - }, - widgets::Background, -}; - -use crate::tab::Tab; -use crate::TabTheme; - -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TabBarProps { - pub tabs: Vec<String>, - pub selected: usize, - pub on_select_tab: Handler<usize>, - #[prop_field(Styles)] - pub styles: Option<Style>, -} - -/// A widget displaying a collection of tabs in a horizontal bar -#[widget] -pub fn TabBar(props: TabBarProps) { - let TabBarProps { - on_select_tab, - selected, - tabs, - .. - } = props.clone(); - let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); - - let tabs = tabs.into_iter().enumerate().map(move |(index, tab)| { - let on_select = on_select_tab.clone(); - let tab_event_handler = OnEvent::new(move |_, event| { - match event.event_type { - EventType::Click(..) => { - on_select.call(index); - } - EventType::KeyDown(evt) => { - if evt.key() == KeyCode::Return || evt.key() == KeyCode::Space { - // We want the focused tab to also be selected by `Enter` or `Space` - on_select.call(index); - } - } - _ => {} - } - }); - - constructor! { - <Tab content={tab.clone()} on_event={Some(tab_event_handler.clone())} selected={selected == index} /> - } - }); - - let background_styles = Style { - layout_type: StyleProp::Value(LayoutType::Row), - background_color: StyleProp::Value(theme.get().bg), - height: StyleProp::Value(Units::Auto), - width: StyleProp::Value(Units::Stretch(1.0)), - ..props.styles.clone().unwrap_or_default() - }; - - rsx! { - <Background styles={Some(background_styles)}> - {VecTracker::from(tabs.clone())} - </Background> - } -} diff --git a/examples/tabs/tab_box.rs b/examples/tabs/tab_box.rs deleted file mode 100644 index 8de0c47..0000000 --- a/examples/tabs/tab_box.rs +++ /dev/null @@ -1,65 +0,0 @@ -use kayak_ui::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - use_state, widget, Bound, Fragment, Handler, WidgetProps, -}; -use std::fmt::Debug; - -use crate::tab_bar::TabBar; -use crate::tab_content::TabContent; -use crate::TabTheme; - -#[derive(Debug, Default, Clone, PartialEq)] -pub struct TabData { - /// The name of this tab - pub name: String, - /// The content to display for this tab, wrapped in a [Fragment] - pub content: Fragment, -} - -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TabBoxProps { - pub initial_tab: usize, - pub tabs: Vec<TabData>, - #[prop_field(Styles)] - pub styles: Option<Style>, -} - -/// The actual tab container widget. -/// -/// This houses both the tab bar and its content. -#[widget] -pub fn TabBox(props: TabBoxProps) { - let TabBoxProps { - initial_tab, tabs, .. - } = props.clone(); - let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); - let (selected, set_selected, ..) = use_state!(initial_tab); - - let tab_names = tabs - .iter() - .map(|tab| tab.name.clone()) - .collect::<Vec<String>>(); - let tab_content = tabs - .iter() - .map(|tab| tab.content.clone()) - .collect::<Vec<_>>(); - - let on_select_tab = Handler::<usize>::new(move |index| { - set_selected(index); - }); - - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Quad), - background_color: StyleProp::Value(theme.get().fg), - ..Default::default() - }); - - rsx! { - <> - <TabBar tabs={tab_names} selected={selected} on_select_tab={on_select_tab} /> - <TabContent tabs={tab_content} selected={selected} /> - </> - } -} diff --git a/examples/tabs/tab_button.rs b/examples/tabs/tab_button.rs new file mode 100644 index 0000000..a1ec59d --- /dev/null +++ b/examples/tabs/tab_button.rs @@ -0,0 +1,91 @@ +use bevy::prelude::{Bundle, Changed, Color, Commands, Component, Entity, In, Query}; +use kayak_ui::prelude::{ + rsx, + widgets::{KButtonBundle, TextProps, TextWidgetBundle}, + Event, EventDispatcherContext, EventType, KChildren, KStyle, OnEvent, StyleProp, Units, Widget, + WidgetContext, WidgetName, +}; + +use crate::tab_context::TabContext; + +#[derive(Component, Default)] +pub struct TabButton { + pub index: usize, + pub title: String, +} + +impl Widget for TabButton {} + +#[derive(Bundle)] +pub struct TabButtonBundle { + pub tab_button: TabButton, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for TabButtonBundle { + fn default() -> Self { + Self { + tab_button: Default::default(), + styles: Default::default(), + widget_name: TabButton::default().get_name(), + } + } +} + +pub fn tab_button_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query<&TabButton>, + tab_context_query: Query<&mut TabContext, Changed<TabContext>>, +) -> bool { + if !tab_context_query.is_empty() { + if let Ok(tab_button) = query.get(entity) { + let context_entity = widget_context + .get_context_entity::<TabContext>(entity) + .unwrap(); + if let Ok(tab_context) = tab_context_query.get(context_entity) { + let background_color = if tab_context.current_index == tab_button.index { + Color::rgba(0.0781, 0.0898, 0.101, 1.0) + } else { + Color::rgba(0.0781, 0.0898, 0.101, 0.75) + }; + let parent_id = Some(entity); + + let button_index = tab_button.index; + let on_event = OnEvent::new( + move |In((event_dispatcher_context, event, _entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut query: Query<&mut TabContext>| { + match event.event_type { + EventType::Click(..) => { + if let Ok(mut tab_context) = query.get_mut(context_entity) { + tab_context.current_index = button_index; + } + } + _ => {} + } + (event_dispatcher_context, event) + }, + ); + + rsx! { + <KButtonBundle + on_event={on_event} + styles={KStyle { + background_color: StyleProp::Value(background_color), + height: StyleProp::Value(Units::Pixels(25.0)), + ..Default::default() + }}> + <TextWidgetBundle text={TextProps { content: tab_button.title.clone(), ..Default::default() }} /> + </KButtonBundle> + } + return true; + } + } + } + false +} diff --git a/examples/tabs/tab_content.rs b/examples/tabs/tab_content.rs deleted file mode 100644 index 3d3c4da..0000000 --- a/examples/tabs/tab_content.rs +++ /dev/null @@ -1,44 +0,0 @@ -use kayak_ui::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Bound, Fragment, VecTracker, WidgetProps, -}; -use std::ops::Index; - -use crate::TabTheme; - -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TabContentProps { - pub selected: usize, - pub tabs: Vec<Fragment>, - #[prop_field(Styles)] - pub styles: Option<Style>, -} - -/// A widget that displays the selected tab's content -#[widget] -pub fn TabContent(props: TabContentProps) { - let TabContentProps { selected, tabs, .. } = props.clone(); - let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); - - if selected >= tabs.len() { - // Invalid tab -> don't do anything - return; - } - - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Quad), - background_color: StyleProp::Value(theme.get().fg), - ..Default::default() - }); - - let tab = tabs.index(selected).clone(); - let tab = vec![tab.clone()]; - - rsx! { - <> - {VecTracker::from(tab.clone().into_iter())} - </> - } -} diff --git a/examples/tabs/tab_context.rs b/examples/tabs/tab_context.rs new file mode 100644 index 0000000..b4682dc --- /dev/null +++ b/examples/tabs/tab_context.rs @@ -0,0 +1,54 @@ +use bevy::prelude::{Bundle, Changed, Commands, Component, Entity, In, Query}; +use kayak_ui::prelude::{KChildren, Widget, WidgetContext, WidgetName}; + +#[derive(Component, Default)] +pub struct TabContext { + pub current_index: usize, +} + +#[derive(Component, Default)] +pub struct TabContextProvider { + pub initial_index: usize, +} + +impl Widget for TabContextProvider {} + +#[derive(Bundle)] +pub struct TabContextProviderBundle { + pub tab_provider: TabContextProvider, + pub children: KChildren, + pub widget_name: WidgetName, +} + +impl Default for TabContextProviderBundle { + fn default() -> Self { + Self { + tab_provider: Default::default(), + children: Default::default(), + widget_name: TabContextProvider::default().get_name(), + } + } +} + +pub fn tab_context_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query< + (&KChildren, &TabContextProvider), + (Changed<KChildren>, Changed<TabContextProvider>), + >, +) -> bool { + if let Ok((children, tab_context_provider)) = query.get(entity) { + let context_entity = commands + .spawn(TabContext { + current_index: tab_context_provider.initial_index, + }) + .id(); + widget_context.set_context_entity::<TabContext>(Some(entity), context_entity); + + children.process(&widget_context, Some(entity)); + + return true; + } + false +} diff --git a/examples/tabs/tabs.rs b/examples/tabs/tabs.rs index 6930af3..cc1311c 100644 --- a/examples/tabs/tabs.rs +++ b/examples/tabs/tabs.rs @@ -1,95 +1,22 @@ -//! This example demonstrates how one might create a tab system -//! -//! Additionally, it showcases focus navigation. Press `Tab` and `Shift + Tab` to move -//! between focusable widgets. This example also sets it up so that `Enter` or `Space` -//! can be used in place of a normal click. - use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, + prelude::{App as BevyApp, AssetServer, Commands, ImageSettings, Res, ResMut, Vec2}, DefaultPlugins, }; - -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{ - constructor, render, rsx, - styles::{Style, StyleProp, Units}, - widget, Color, - }, - widgets::{App, Text, Window}, -}; - -use crate::theming::{ColorState, TabTheme, TabThemeProvider}; -use tab_box::TabBox; -use tab_box::TabData; +use kayak_ui::prelude::{widgets::*, *}; mod tab; -mod tab_bar; -mod tab_box; -mod tab_content; -mod theming; - -#[widget] -fn TabDemo() { - let text_style = Style { - width: StyleProp::Value(Units::Percentage(75.0)), - top: StyleProp::Value(Units::Stretch(0.5)), - left: StyleProp::Value(Units::Stretch(1.0)), - bottom: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - // TODO: This is not the most ideal way to generate tabs. For one, the `content` has no access to its actual context - // (i.e. where it actually exists in the hierarchy). Additionally, it would be better if tabs were created as - // children of `TabBox`. These are issues that will be addressed in the future, so for now, this will work. - let tabs = vec![ - TabData { - name: "Tab 1".to_string(), - content: { - let text_style = text_style.clone(); - constructor! { - <> - <Text content={"Welcome to Tab 1!".to_string()} size={48.0} styles={Some(text_style)} /> - </> - } - }, - }, - TabData { - name: "Tab 2".to_string(), - content: { - let text_style = text_style.clone(); - constructor! { - <> - <Text content={"Welcome to Tab 2!".to_string()} size={48.0} styles={Some(text_style)} /> - </> - } - }, - }, - TabData { - name: "Tab 3".to_string(), - content: { - let text_style = text_style.clone(); - constructor! { - <> - <Text content={"Welcome to Tab 3!".to_string()} size={48.0} styles={Some(text_style)} /> - </> - } - }, - }, - ]; - - rsx! { - <TabBox tabs={tabs} /> - } -} +mod tab_button; +mod tab_context; +use tab::{tab_update, Tab, TabBundle}; +use tab_button::{tab_button_update, TabButton, TabButtonBundle}; +use tab_context::{tab_context_update, TabContextProvider, TabContextProviderBundle}; fn startup( mut commands: Commands, mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, ) { +<<<<<<< HEAD commands.spawn_bundle(UICameraBundle::new()); font_mapping.set_default(asset_server.load("roboto.kayak_font")); @@ -130,10 +57,58 @@ fn startup( }); commands.insert_resource(context); +======= + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + widget_context.add_widget_system(Tab::default().get_name(), tab_update); + widget_context.add_widget_system(TabContextProvider::default().get_name(), tab_context_update); + widget_context.add_widget_system(TabButton::default().get_name(), tab_button_update); + let parent_id = None; + + rsx! { + <KayakAppBundle> + <WindowBundle + window={KWindow { + title: "Tabs".into(), + draggable: true, + position: Vec2::new(10.0, 10.0), + size: Vec2::new(300.0, 250.0), + ..KWindow::default() + }} + > + <TabContextProviderBundle tab_provider={TabContextProvider { initial_index: 0 }}> + <ElementBundle + styles={KStyle { + layout_type: StyleProp::Value(LayoutType::Row), + height: StyleProp::Value(Units::Auto), + width: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }} + > + <TabButtonBundle tab_button={TabButton { index: 0, title: "Tab 1".into() }} /> + <TabButtonBundle tab_button={TabButton { index: 1, title: "Tab 2".into() }} /> + </ElementBundle> + <TabBundle tab={Tab { index: 0 }}> + <TextWidgetBundle text={TextProps { content: "Tab 1".into(), ..Default::default() }} /> + </TabBundle> + <TabBundle tab={Tab { index: 1 }}> + <TextWidgetBundle text={TextProps { content: "Tab 2".into(), ..Default::default() }} /> + </TabBundle> + </TabContextProviderBundle> + </WindowBundle> + </KayakAppBundle> + } + + commands.insert_resource(widget_context); +>>>>>>> exp/main } fn main() { BevyApp::new() +<<<<<<< HEAD .insert_resource(WindowDescriptor { width: 1270.0, height: 720.0, @@ -144,4 +119,12 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); +======= + .insert_resource(ImageSettings::default_nearest()) + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +>>>>>>> exp/main } diff --git a/examples/tabs/theming.rs b/examples/tabs/theming.rs deleted file mode 100644 index e8feaa4..0000000 --- a/examples/tabs/theming.rs +++ /dev/null @@ -1,37 +0,0 @@ -use kayak_ui::core::{rsx, widget, Children, Color, WidgetProps}; - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct TabTheme { - pub primary: Color, - pub bg: Color, - pub fg: Color, - pub focus: Color, - pub text: ColorState, - pub active_tab: ColorState, - pub inactive_tab: ColorState, - pub tab_height: f32, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct ColorState { - pub normal: Color, - pub hovered: Color, - pub active: Color, -} - -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TabThemeProviderProps { - pub initial_theme: TabTheme, - #[prop_field(Children)] - pub children: Option<Children>, -} - -#[widget] -pub fn TabThemeProvider(props: TabThemeProviderProps) { - let TabThemeProviderProps { - initial_theme, - children, - } = props.clone(); - context.create_provider(initial_theme); - rsx! { <>{children}</> } -} diff --git a/examples/text.rs b/examples/text.rs new file mode 100644 index 0000000..b0bdaf2 --- /dev/null +++ b/examples/text.rs @@ -0,0 +1,90 @@ +use bevy::{ + prelude::{ + App as BevyApp, AssetServer, Bundle, Commands, Component, Entity, In, Input, KeyCode, + Query, Res, ResMut, Resource, + }, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; + +#[derive(Component, Default)] +pub struct MyWidgetProps { + pub foo: u32, +} + +fn my_widget_1_update( + In((_widget_context, entity)): In<(WidgetContext, Entity)>, + my_resource: Res<MyResource>, + mut query: Query<(&mut MyWidgetProps, &mut KStyle)>, +) -> bool { + if my_resource.is_changed() || my_resource.is_added() { + if let Ok((mut my_widget, mut style)) = query.get_mut(entity) { + my_widget.foo = my_resource.0; + dbg!(my_widget.foo); + style.render_command = StyleProp::Value(RenderCommand::Text { + content: format!("My number is: {}", my_widget.foo).to_string(), + alignment: Alignment::Start, + }); + return true; + } + } + + false +} + +impl Widget for MyWidgetProps {} + +#[derive(Bundle)] +pub struct MyWidgetBundle { + props: MyWidgetProps, + styles: KStyle, + widget_name: WidgetName, +} + +impl Default for MyWidgetBundle { + fn default() -> Self { + Self { + props: Default::default(), + styles: Default::default(), + widget_name: MyWidgetProps::default().get_name(), + } + } +} + +#[derive(Resource)] +pub struct MyResource(pub u32); + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + let parent_id = None; + widget_context.add_widget_system(MyWidgetProps::default().get_name(), my_widget_1_update); + rsx! { + <KayakAppBundle><MyWidgetBundle props={MyWidgetProps { foo: 0 }} /></KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn update_resource(keyboard_input: Res<Input<KeyCode>>, mut my_resource: ResMut<MyResource>) { + if keyboard_input.just_pressed(KeyCode::Space) { + my_resource.0 += 1; + } +} + +fn main() { + BevyApp::new() + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .insert_resource(MyResource(1)) + .add_startup_system(startup) + .add_system(update_resource) + .run() +} diff --git a/examples/text_box.rs b/examples/text_box.rs index 3342b60..76e38dc 100644 --- a/examples/text_box.rs +++ b/examples/text_box.rs @@ -1,80 +1,109 @@ use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, + prelude::{ + Added, App as BevyApp, AssetServer, Bundle, Changed, Commands, Component, Entity, In, Or, + ParamSet, Query, Res, ResMut, Vec2, With, + }, DefaultPlugins, }; -use kayak_core::Color; -use kayak_render_macros::use_state; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{Style, StyleProp, Units}, - widget, -}; -use kayak_ui::widgets::{App, OnChange, SpinBox, SpinBoxStyle, TextBox, Window}; - -#[widget] -fn TextBoxExample() { - let (value, set_value, _) = use_state!("I started with a value!".to_string()); - let (empty_value, set_empty_value, _) = use_state!("".to_string()); - let (red_value, set_red_value, _) = use_state!("This text is red".to_string()); - let (spin_value, set_spin_value, _) = use_state!("3".to_string()); - - let input_styles = Style { - top: StyleProp::Value(Units::Pixels(10.0)), - ..Default::default() - }; - - let red_text_styles = Style { - color: StyleProp::Value(Color::new(1., 0., 0., 1.)), - ..input_styles.clone() - }; - - let on_change = OnChange::new(move |event| { - set_value(event.value); - }); +use kayak_ui::prelude::{widgets::*, *}; - let on_change_empty = OnChange::new(move |event| { - set_empty_value(event.value); - }); +#[derive(Component, Default)] +struct TextBoxExample; - let on_change_red = OnChange::new(move |event| { - set_red_value(event.value); - }); +#[derive(Component, Default)] +struct TextBoxExampleState { + pub value1: String, + pub value2: String, +} - let on_change_spin = OnChange::new(move |event| { - set_spin_value(event.value); - }); +impl Widget for TextBoxExample {} + +#[derive(Bundle)] +struct TextBoxExampleBundle { + text_box_example: TextBoxExample, + styles: KStyle, + widget_name: WidgetName, +} - let vert = SpinBoxStyle::Vertical; +impl Default for TextBoxExampleBundle { + fn default() -> Self { + Self { + text_box_example: Default::default(), + styles: Default::default(), + widget_name: TextBoxExample::default().get_name(), + } + } +} - rsx! { - <Window position={(50.0, 50.0)} size={(500.0, 300.0)} title={"TextBox Example".to_string()}> - <TextBox styles={Some(input_styles)} value={value} on_change={Some(on_change)} /> - <TextBox - styles={Some(input_styles)} - value={empty_value} - on_change={Some(on_change_empty)} - placeholder={Some("This is a placeholder".to_string())} - /> - <TextBox styles={Some(red_text_styles)} value={red_value} on_change={Some(on_change_red)} /> - <SpinBox - styles={Some(input_styles)} - value={spin_value} - on_change={Some(on_change_spin)} - min_val={0.0} - max_val={10.0} - /> - <SpinBox - spin_button_style={vert} - styles={Some(input_styles)} - value={spin_value} - on_change={Some(on_change_spin)} - min_val={0.0} - max_val={10.0} - /> - </Window> +fn update_text_box_example( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + props_query: Query< + &TextBoxExample, + Or<(Changed<TextBoxExample>, Changed<KStyle>, With<Mounted>)>, + >, + mut state_query: ParamSet<( + Query<Entity, Or<(Added<TextBoxExampleState>, Changed<TextBoxExampleState>)>>, + Query<&TextBoxExampleState>, + )>, +) -> bool { + if !props_query.is_empty() || !state_query.p0().is_empty() { + let state_entity = widget_context.get_context_entity::<TextBoxExampleState>(entity); + if state_entity.is_none() { + let state_entity = commands + .spawn(TextBoxExampleState { + value1: "Hello World".into(), + value2: "Hello World2".into(), + }) + .id(); + widget_context.set_context_entity::<TextBoxExampleState>(Some(entity), state_entity); + return false; + } + let state_entity = state_entity.unwrap(); + + let p1 = state_query.p1(); + let textbox_state = p1.get(state_entity).unwrap(); + + let on_change = OnChange::new( + move |In((_widget_context, _, value)): In<(WidgetContext, Entity, String)>, + mut state_query: Query<&mut TextBoxExampleState>| { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.value1 = value; + } + }, + ); + + let on_change2 = OnChange::new( + move |In((_widget_context, _, value)): In<(WidgetContext, Entity, String)>, + mut state_query: Query<&mut TextBoxExampleState>| { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.value2 = value; + } + }, + ); + + let parent_id = Some(entity); + rsx! { + <ElementBundle> + <TextBoxBundle + styles={KStyle { + bottom: StyleProp::Value(Units::Pixels(10.0)), + ..Default::default() + }} + text_box={TextBoxProps { value: textbox_state.value1.clone(), ..Default::default()}} + on_change={on_change} + /> + <TextBoxBundle + text_box={TextBoxProps { value: textbox_state.value2.clone(), ..Default::default()}} + on_change={on_change2} + /> + </ElementBundle> + } + + return true; } + + false } fn startup( @@ -82,6 +111,7 @@ fn startup( mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, ) { +<<<<<<< HEAD commands.spawn_bundle(UICameraBundle::new()); font_mapping.set_default(asset_server.load("roboto.kayak_font")); @@ -95,10 +125,39 @@ fn startup( }); commands.insert_resource(context); +======= + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + widget_context.add_widget_system( + TextBoxExample::default().get_name(), + update_text_box_example, + ); + let parent_id = None; + rsx! { + <KayakAppBundle> + <WindowBundle + window={KWindow { + title: "Hello text box".into(), + draggable: true, + position: Vec2::new(10.0, 10.0), + size: Vec2::new(300.0, 250.0), + ..KWindow::default() + }} + > + <TextBoxExampleBundle /> + </WindowBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); +>>>>>>> exp/main } fn main() { BevyApp::new() +<<<<<<< HEAD .insert_resource(WindowDescriptor { width: 1270.0, height: 720.0, @@ -109,4 +168,11 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); +======= + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +>>>>>>> exp/main } diff --git a/examples/texture_atlas.rs b/examples/texture_atlas.rs index 4d09377..16a4754 100644 --- a/examples/texture_atlas.rs +++ b/examples/texture_atlas.rs @@ -1,36 +1,31 @@ use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut}, - render::texture::ImageSettings, - window::WindowDescriptor, + prelude::{App as BevyApp, AssetServer, Commands, ImageSettings, Res, ResMut}, DefaultPlugins, }; -use kayak_core::styles::PositionType; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, ImageManager, UICameraBundle}; -use kayak_ui::core::{ - render, - styles::{Style, StyleProp, Units}, -}; -use kayak_ui::widgets::{App, TextureAtlas}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; fn startup( mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, - mut image_manager: ResMut<ImageManager>, ) { - commands.spawn_bundle(UICameraBundle::new()); + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); - let image_handle: Handle<bevy::render::texture::Image> = asset_server.load("texture_atlas.png"); - let ui_image_handle = image_manager.get(&image_handle); + let image_handle = asset_server.load("texture_atlas.png"); //texture_atlas.png uses 16 pixel sprites and is 272x128 pixels let tile_size = 16; let columns = 272 / tile_size; let rows = 128 / tile_size; let atlas = bevy::sprite::TextureAtlas::from_grid( - image_handle, + image_handle.clone(), bevy::prelude::Vec2::splat(tile_size as f32), columns, rows, + None, + None, ); //The sign in the top right of the image would be index 16 @@ -38,52 +33,54 @@ fn startup( //The flower is in the 6(-1) row and 15 collumn let flower_index = columns * 5 + 15; - let context = BevyContext::new(|context| { - let atlas_styles = Style { - position_type: StyleProp::Value(PositionType::ParentDirected), - width: StyleProp::Value(Units::Pixels(200.0)), - height: StyleProp::Value(Units::Pixels(200.0)), - ..Style::default() - }; + let mut widget_context = Context::new(); + let parent_id = None; + + let atlas_styles = KStyle { + position_type: StyleProp::Value(PositionType::ParentDirected), + width: StyleProp::Value(Units::Pixels(200.0)), + height: StyleProp::Value(Units::Pixels(200.0)), + ..KStyle::default() + }; - let rect = atlas.textures[sign_index]; - let sign_position = rect.min; - let sign_size = rect.max - rect.min; + let rect = atlas.textures[sign_index]; + let sign_position = rect.min; + let sign_size = rect.max - rect.min; - let rect = atlas.textures[flower_index]; - let flower_position = rect.min; - let flower_size = rect.max - rect.min; + let rect = atlas.textures[flower_index]; + let flower_position = rect.min; + let flower_size = rect.max - rect.min; - render! { - <App> - <TextureAtlas styles={Some(atlas_styles)} - handle={ui_image_handle} - position={(sign_position.x, sign_position.y)} - tile_size={(sign_size.x, sign_size.y)} - /> - <TextureAtlas styles={Some(atlas_styles)} - handle={ui_image_handle} - position={(flower_position.x, flower_position.y)} - tile_size={(flower_size.x, flower_size.y)} - /> - </App> - } - }); + rsx! { + <KayakAppBundle> + <TextureAtlasBundle + atlas={TextureAtlas { + handle: image_handle.clone(), + position: sign_position, + tile_size: sign_size, + }} + styles={atlas_styles.clone()} + /> + <TextureAtlasBundle + atlas={TextureAtlas { + handle: image_handle.clone(), + position: flower_position, + tile_size: flower_size, + }} + styles={atlas_styles.clone()} + /> + </KayakAppBundle> + } - commands.insert_resource(context); + commands.insert_resource(widget_context); } fn main() { BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) .insert_resource(ImageSettings::default_nearest()) .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) .add_startup_system(startup) - .run(); + .run() } diff --git a/examples/todo/add_button.rs b/examples/todo/add_button.rs deleted file mode 100644 index a127545..0000000 --- a/examples/todo/add_button.rs +++ /dev/null @@ -1,61 +0,0 @@ -use kayak_core::CursorIcon; -use kayak_ui::core::{ - color::Color, - render_command::RenderCommand, - rsx, - styles::{Corner, Style, StyleProp, Units}, - use_state, widget, EventType, OnEvent, WidgetProps, -}; - -use kayak_ui::widgets::{Background, Text}; - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -pub struct AddButtonProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, -} - -#[widget] -pub fn AddButton(props: AddButtonProps) { - let (color, set_color, ..) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0)); - - let base_styles = props.styles.clone().unwrap_or_default(); - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - height: StyleProp::Value(Units::Pixels(32.0)), - width: StyleProp::Value(Units::Pixels(30.0)), - ..base_styles - }); - - let background_styles = Some(Style { - border_radius: StyleProp::Value(Corner::all(5.0)), - background_color: StyleProp::Value(color), - cursor: CursorIcon::Hand.into(), - padding_left: StyleProp::Value(Units::Pixels(9.0)), - padding_bottom: StyleProp::Value(Units::Pixels(6.0)), - ..Style::default() - }); - - let text_styles = Some(Style { - cursor: StyleProp::Inherit, - ..Style::default() - }); - - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::MouseIn(..) => { - set_color(Color::new(0.0791, 0.0998, 0.201, 1.0)); - } - EventType::MouseOut(..) => { - set_color(Color::new(0.0781, 0.0898, 0.101, 1.0)); - } - _ => {} - }); - - rsx! { - <Background styles={background_styles} on_event={Some(on_event)}> - <Text content={"+".to_string()} size={20.0} styles={text_styles} /> - </Background> - } -} diff --git a/examples/todo/card.rs b/examples/todo/card.rs deleted file mode 100644 index 52f5b9b..0000000 --- a/examples/todo/card.rs +++ /dev/null @@ -1,49 +0,0 @@ -use kayak_core::styles::Edge; -use kayak_ui::core::{ - rsx, - styles::{LayoutType, Style, StyleProp, Units}, - widget, Color, EventType, Handler, OnEvent, WidgetProps, -}; -use kayak_ui::widgets::{Background, Text}; - -use super::delete_button::DeleteButton; - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -pub struct CardProps { - pub card_id: usize, - pub name: String, - pub on_delete: Handler<usize>, -} - -#[widget] -pub fn Card(props: CardProps) { - let CardProps { - card_id, - name, - on_delete, - } = props.clone(); - let background_styles = Style { - layout_type: StyleProp::Value(LayoutType::Row), - background_color: StyleProp::Value(Color::new(0.176, 0.196, 0.215, 1.0)), - height: StyleProp::Value(Units::Auto), - min_height: StyleProp::Value(Units::Pixels(26.0)), - top: StyleProp::Value(Units::Pixels(10.0)), - padding: StyleProp::Value(Edge::all(Units::Pixels(5.0))), - ..Style::default() - }; - - let on_delete = on_delete.clone(); - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - on_delete.call(card_id); - } - _ => (), - }); - - rsx! { - <Background styles={Some(background_styles)}> - <Text line_height={Some(26.0)} size={14.0} content={name} /> - <DeleteButton on_event={Some(on_event)} /> - </Background> - } -} diff --git a/examples/todo/cards.rs b/examples/todo/cards.rs deleted file mode 100644 index 88e6ba4..0000000 --- a/examples/todo/cards.rs +++ /dev/null @@ -1,26 +0,0 @@ -use kayak_ui::core::{constructor, rsx, widget, Handler, VecTracker, WidgetProps}; -use kayak_ui::widgets::Element; - -use super::{card::Card, Todo}; - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -pub struct CardsProps { - pub cards: Vec<Todo>, - pub on_delete: Handler<usize>, -} - -#[widget] -pub fn Cards(props: CardsProps) { - let CardsProps { cards, on_delete } = props.clone(); - rsx! { - <Element> - {VecTracker::from( - cards - .clone() - .into_iter() - .enumerate() - .map(|(index, todo)| constructor! { <Card card_id={index} name={todo.name.clone()} on_delete={on_delete.clone()} /> }), - )} - </Element> - } -} diff --git a/examples/todo/delete_button.rs b/examples/todo/delete_button.rs deleted file mode 100644 index 51951b5..0000000 --- a/examples/todo/delete_button.rs +++ /dev/null @@ -1,61 +0,0 @@ -use kayak_core::CursorIcon; -use kayak_ui::core::{ - color::Color, - render_command::RenderCommand, - rsx, - styles::{Corner, Style, StyleProp, Units}, - use_state, widget, EventType, OnEvent, WidgetProps, -}; - -use kayak_ui::widgets::{Background, Text}; - -#[derive(WidgetProps, Clone, Debug, Default, PartialEq)] -pub struct DeleteButtonProps { - #[prop_field(Styles)] - styles: Option<Style>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, -} - -#[widget] -pub fn DeleteButton(props: DeleteButtonProps) { - let (color, set_color, ..) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0)); - - let base_styles = props.styles.clone().unwrap_or_default(); - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - height: StyleProp::Value(Units::Pixels(32.0)), - width: StyleProp::Value(Units::Pixels(30.0)), - left: StyleProp::Value(Units::Stretch(1.0)), - ..base_styles - }); - - let background_styles = Some(Style { - border_radius: StyleProp::Value(Corner::all(5.0)), - background_color: StyleProp::Value(color), - cursor: CursorIcon::Hand.into(), - padding_left: StyleProp::Value(Units::Pixels(8.0)), - ..Style::default() - }); - - let text_styles = Some(Style { - cursor: StyleProp::Inherit, - ..Style::default() - }); - - let on_event = OnEvent::new(move |_, event| match event.event_type { - EventType::MouseIn(..) => { - set_color(Color::new(0.0791, 0.0998, 0.201, 1.0)); - } - EventType::MouseOut(..) => { - set_color(Color::new(0.0781, 0.0898, 0.101, 1.0)); - } - _ => {} - }); - - rsx! { - <Background styles={background_styles} on_event={Some(on_event)}> - <Text content={"X".to_string()} size={20.0} styles={text_styles} /> - </Background> - } -} diff --git a/examples/todo/input.rs b/examples/todo/input.rs new file mode 100644 index 0000000..ef8a227 --- /dev/null +++ b/examples/todo/input.rs @@ -0,0 +1,156 @@ +use bevy::prelude::*; +use kayak_ui::prelude::{widgets::*, *}; + +use crate::TodoList; + +#[derive(Component, Default)] +pub struct TodoInputProps { + has_focus: bool, +} + +impl Widget for TodoInputProps {} + +#[derive(Bundle)] +pub struct TodoInputBundle { + pub widget: TodoInputProps, + pub focusable: Focusable, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for TodoInputBundle { + fn default() -> Self { + Self { + widget: TodoInputProps::default(), + focusable: Default::default(), + styles: KStyle { + render_command: StyleProp::Value(RenderCommand::Layout), + // height: StyleProp::Value(Units::Stretch(1.0)), + height: StyleProp::Value(Units::Auto), + width: StyleProp::Value(Units::Stretch(1.0)), + bottom: StyleProp::Value(Units::Pixels(20.0)), + ..KStyle::default() + }, + widget_name: TodoInputProps::default().get_name(), + } + } +} + +pub fn update_todo_input( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut todo_list: ResMut<TodoList>, + keyboard_input: Res<Input<KeyCode>>, + change_query: Query<Entity, (With<TodoInputProps>, With<Mounted>)>, + prop_query: Query<&TodoInputProps>, +) -> bool { + if todo_list.is_changed() || !change_query.is_empty() { + if let Ok(props) = prop_query.get(entity) { + let on_change = OnChange::new( + move |In((_widget_context, _, value)): In<(WidgetContext, Entity, String)>, + mut todo_list: ResMut<TodoList>| { + todo_list.new_item = value; + }, + ); + + if keyboard_input.just_pressed(KeyCode::Return) { + if props.has_focus { + let value = todo_list.new_item.clone(); + todo_list.items.push(value); + todo_list.new_item.clear(); + } + } + + let handle_click = OnEvent::new( + move |In((event_dispatcher_context, event, _)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut todo_list: ResMut<TodoList>| { + match event.event_type { + EventType::Click(..) => { + let value = todo_list.new_item.clone(); + todo_list.items.push(value); + todo_list.new_item.clear(); + } + _ => {} + } + (event_dispatcher_context, event) + }, + ); + + let handle_focus = OnEvent::new( + move |In((event_dispatcher_context, event, _)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut props_query: Query<&mut TodoInputProps>| { + if let Ok(mut props) = props_query.get_mut(entity) { + match event.event_type { + EventType::Focus => { + props.has_focus = true; + } + EventType::Blur => { + props.has_focus = false; + } + _ => {} + } + } + (event_dispatcher_context, event) + }, + ); + + let parent_id = Some(entity); + rsx! { + <ElementBundle + id={"element_bundle"} + styles={KStyle { + layout_type: StyleProp::Value(LayoutType::Row), + height: StyleProp::Value(Units::Pixels(32.0)), + cursor: StyleProp::Value(KCursorIcon(CursorIcon::Text)), + ..Default::default() + }} + on_event={handle_focus} + > + { + // You can spawn whatever you want on UI widgets this way! :) + commands.entity(element_bundle).insert(Focusable); + } + <TextBoxBundle + styles={KStyle { + bottom: StyleProp::Value(Units::Stretch(1.0)), + top: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }} + text_box={TextBoxProps { + value: todo_list.new_item.clone(), + placeholder: Some("Add item here..".into()), + ..Default::default() + }} + on_change={on_change} + /> + <KButtonBundle + styles={KStyle { + width: StyleProp::Value(Units::Pixels(32.0)), + height: StyleProp::Value(Units::Pixels(32.0)), + left: StyleProp::Value(Units::Pixels(5.0)), + ..Default::default() + }} + on_event={handle_click} + > + <TextWidgetBundle + text={TextProps { + content: "+".into(), + ..Default::default() + }} + /> + </KButtonBundle> + </ElementBundle> + } + return true; + } + } + false +} diff --git a/examples/todo/items.rs b/examples/todo/items.rs new file mode 100644 index 0000000..dc084b4 --- /dev/null +++ b/examples/todo/items.rs @@ -0,0 +1,107 @@ +use bevy::prelude::*; +use kayak_ui::prelude::{widgets::*, *}; + +use crate::TodoList; + +#[derive(Component, Default)] +pub struct TodoItemsProps; + +impl Widget for TodoItemsProps {} + +#[derive(Bundle)] +pub struct TodoItemsBundle { + pub widget: TodoItemsProps, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for TodoItemsBundle { + fn default() -> Self { + Self { + widget: TodoItemsProps::default(), + styles: KStyle { + render_command: StyleProp::Value(RenderCommand::Layout), + // height: StyleProp::Value(Units::Stretch(1.0)), + width: StyleProp::Value(Units::Stretch(1.0)), + ..KStyle::default() + }, + widget_name: TodoItemsProps::default().get_name(), + } + } +} + +pub fn update_todo_items( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + todo_list: Res<TodoList>, + query: Query<&TodoItemsProps, Or<(Changed<Style>, Changed<TodoItemsProps>, With<Mounted>)>>, +) -> bool { + if query.is_empty() || todo_list.is_changed() { + let parent_id = Some(entity); + rsx! { + <ElementBundle> + {todo_list.items.iter().enumerate().for_each(|(index, content)| { + let handle_click = OnEvent::new( + move |In((event_dispatcher_context, event, _)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut todo_list: ResMut<TodoList>,| { + match event.event_type { + EventType::Click(..) => { + todo_list.items.remove(index); + }, + _ => {} + } + (event_dispatcher_context, event) + }, + ); + constructor! { + <ElementBundle + styles={KStyle { + render_command: StyleProp::Value(RenderCommand::Quad), + background_color: StyleProp::Value(Color::rgba(0.0781, 0.0898, 0.101, 1.0)), + border_radius: StyleProp::Value(Corner::all(3.0)), + bottom: StyleProp::Value(Units::Pixels(5.0)), + height: StyleProp::Value(Units::Auto), + padding: StyleProp::Value(Edge::all(Units::Pixels(10.0))), + layout_type: StyleProp::Value(LayoutType::Row), + ..Default::default() + }} + > + <TextWidgetBundle + text={TextProps { + content: content.clone(), + ..Default::default() + }} + styles={KStyle { + right: StyleProp::Value(Units::Stretch(1.0)), + top: StyleProp::Value(Units::Stretch(1.0)), + bottom: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }} + /> + <KButtonBundle + styles={KStyle { + width: StyleProp::Value(Units::Pixels(32.0)), + height: StyleProp::Value(Units::Pixels(32.0)), + left: StyleProp::Value(Units::Pixels(15.0)), + ..Default::default() + }} + on_event={handle_click} + > + <TextWidgetBundle text={TextProps { + content: "X".into(), + ..Default::default() + }} /> + </KButtonBundle> + </ElementBundle> + } + })} + </ElementBundle> + } + return true; + } + false +} diff --git a/examples/todo/todo.rs b/examples/todo/todo.rs index 9e0a17a..df441ea 100644 --- a/examples/todo/todo.rs +++ b/examples/todo/todo.rs @@ -1,97 +1,31 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{ - render, rsx, - styles::{LayoutType, Style, StyleProp, Units}, - use_state, widget, EventType, Handler, OnEvent, -}; -use kayak_ui::widgets::{App, Element, OnChange, TextBox, Window}; +use bevy::prelude::*; +use kayak_ui::prelude::{widgets::*, *}; -mod add_button; -mod card; -mod cards; -mod delete_button; -use add_button::AddButton; -use cards::Cards; +mod input; +mod items; -#[derive(Debug, Clone, PartialEq)] -pub struct Todo { - name: String, -} - -#[widget] -fn TodoApp() { - let (todos, set_todos, ..) = use_state!(vec![ - Todo { - name: "Use bevy to make a game!".to_string(), - }, - Todo { - name: "Help contribute to bevy!".to_string(), - }, - Todo { - name: "Join the bevy discord!".to_string(), - }, - ]); - - let (new_todo_value, set_new_todo_value, ..) = use_state!("".to_string()); +use crate::input::*; +use items::*; - let text_box_styles = Style { - right: StyleProp::Value(Units::Pixels(10.0)), - ..Style::default() - }; - - let top_area_styles = Style { - layout_type: StyleProp::Value(LayoutType::Row), - bottom: StyleProp::Value(Units::Pixels(10.0)), - height: StyleProp::Value(Units::Pixels(30.0)), - padding_top: StyleProp::Value(Units::Stretch(1.0)), - padding_bottom: StyleProp::Value(Units::Stretch(1.0)), - ..Style::default() - }; - - let on_change = OnChange::new(move |event| { - set_new_todo_value(event.value); - }); +// A bit of state management. +// Consider this like "global" state. +#[derive(Resource)] +pub struct TodoList { + pub new_item: String, + pub items: Vec<String>, +} - let new_todo_value_cloned = new_todo_value.clone(); - let mut todos_cloned = todos.clone(); - let cloned_set_todos = set_todos.clone(); - let add_events = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - if !new_todo_value_cloned.is_empty() { - todos_cloned.push(Todo { - name: new_todo_value_cloned.clone(), - }); - cloned_set_todos(todos_cloned.clone()); - } +impl TodoList { + pub fn new() -> Self { + Self { + new_item: "".into(), + items: vec![ + "Buy milk".into(), + "Paint Shed".into(), + "Eat Dinner".into(), + "Write new Bevy UI library".into(), + ], } - _ => {} - }); - - let mut todos_cloned = todos.clone(); - let cloned_set_todos = set_todos.clone(); - let handle_delete = Handler::new(move |card_id: usize| { - todos_cloned.remove(card_id); - cloned_set_todos(todos_cloned.clone()); - }); - - rsx! { - <Window draggable={true} position={(415.0, 50.0)} size={(450.0, 600.0)} title={"Todo!".to_string()}> - <Element styles={Some(top_area_styles)}> - <TextBox - styles={Some(text_box_styles)} - value={new_todo_value} - placeholder={Some("Type here to add a new todo!".to_string())} - on_change={Some(on_change)} - /> - <AddButton on_event={Some(add_events)} /> - </Element> - <Cards cards={todos} on_delete={handle_delete} /> - </Window> } } @@ -100,6 +34,7 @@ fn startup( mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, ) { +<<<<<<< HEAD commands.spawn_bundle(UICameraBundle::new()); font_mapping.set_default(asset_server.load("roboto.kayak_font")); @@ -127,4 +62,45 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); +======= + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + widget_context.add_widget_system(TodoItemsProps::default().get_name(), update_todo_items); + widget_context.add_widget_system(TodoInputProps::default().get_name(), update_todo_input); + let parent_id = None; + rsx! { + <KayakAppBundle> + <WindowBundle + window={KWindow { + title: "Todo App".into(), + draggable: true, + position: Vec2::new((1280.0 / 2.0) - (350.0 / 2.0), (720.0 / 2.0) - (600.0 / 2.0)), + size: Vec2::new(400.0, 600.0), + ..Default::default() + }} + > + <TodoInputBundle /> + <ScrollContextProviderBundle> + <ScrollBoxBundle> + <TodoItemsBundle /> + </ScrollBoxBundle> + </ScrollContextProviderBundle> + </WindowBundle> + </KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .insert_non_send_resource(TodoList::new()) + .add_startup_system(startup) + .run() +>>>>>>> exp/main } diff --git a/examples/vec.rs b/examples/vec.rs new file mode 100644 index 0000000..bf85f58 --- /dev/null +++ b/examples/vec.rs @@ -0,0 +1,88 @@ +use bevy::{ + prelude::{ + App as BevyApp, AssetServer, Bundle, Changed, Commands, Component, Entity, In, Or, Query, + Res, ResMut, With, + }, + DefaultPlugins, +}; +use kayak_ui::prelude::{widgets::*, KStyle, *}; + +#[derive(Component, Default)] +pub struct MyWidgetProps {} + +fn my_widget_1_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + query: Query<Entity, Or<(With<Mounted>, Changed<MyWidgetProps>)>>, +) -> bool { + if let Ok(_) = query.get(entity) { + let parent_id = Some(entity); + let data = vec![ + "Text 1", "Text 2", "Text 3", "Text 4", "Text 5", "Text 6", "Text 7", "Text 8", + "Text 9", "Text 10", + ]; + rsx! { + <ElementBundle> + {data.iter().for_each(|text| { + constructor! { + <TextWidgetBundle + text={TextProps { + content: text.clone().into(), + ..Default::default() + }} + /> + } + })} + </ElementBundle> + } + return true; + } + + false +} + +impl Widget for MyWidgetProps {} + +#[derive(Bundle)] +pub struct MyWidgetBundle { + props: MyWidgetProps, + styles: KStyle, + widget_name: WidgetName, +} + +impl Default for MyWidgetBundle { + fn default() -> Self { + Self { + props: Default::default(), + styles: Default::default(), + widget_name: MyWidgetProps::default().get_name(), + } + } +} + +fn startup( + mut commands: Commands, + mut font_mapping: ResMut<FontMapping>, + asset_server: Res<AssetServer>, +) { + font_mapping.set_default(asset_server.load("roboto.kayak_font")); + + commands.spawn(UICameraBundle::new()); + + let mut widget_context = Context::new(); + let parent_id = None; + widget_context.add_widget_system(MyWidgetProps::default().get_name(), my_widget_1_update); + rsx! { + <KayakAppBundle><MyWidgetBundle /></KayakAppBundle> + } + commands.insert_resource(widget_context); +} + +fn main() { + BevyApp::new() + .add_plugins(DefaultPlugins) + .add_plugin(ContextPlugin) + .add_plugin(KayakWidgets) + .add_startup_system(startup) + .run() +} diff --git a/examples/vec_widget.rs b/examples/vec_widget.rs deleted file mode 100644 index f6103c0..0000000 --- a/examples/vec_widget.rs +++ /dev/null @@ -1,50 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{constructor, render, VecTracker}; -use kayak_ui::widgets::{App, Text}; - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - let data = vec![ - "Text 1", "Text 2", "Text 3", "Text 4", "Text 5", "Text 6", "Text 7", "Text 8", - "Text 9", "Text 10", - ]; - render! { - <App> - {VecTracker::from(data.iter().map(|data| { - constructor! { - <Text content={data.clone().to_string()} size={16.0} /> - } - }))} - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/widget_template.rs b/examples/widget_template.rs new file mode 100644 index 0000000..fad397b --- /dev/null +++ b/examples/widget_template.rs @@ -0,0 +1,51 @@ +/// This is a simple widget template. +/// It'll auto update if the props change. +/// Don't forget to register the update system with kayak! +use bevy::prelude::*; +use kayak_ui::prelude::*; + +#[derive(Component, Default)] +pub struct WidgetProps; + +impl Widget for WidgetProps {} + +#[derive(Bundle)] +pub struct WidgetBundle { + pub widget: WidgetProps, + pub styles: KStyle, + pub children: KChildren, + pub widget_name: WidgetName, +} + +impl Default for WidgetBundle { + fn default() -> Self { + Self { + widget: WidgetProps::default(), + styles: KStyle { + render_command: StyleProp::Value(RenderCommand::Clip), + height: StyleProp::Value(Units::Stretch(1.0)), + width: StyleProp::Value(Units::Stretch(1.0)), + ..KStyle::default() + }, + children: KChildren::default(), + widget_name: WidgetProps::default().get_name(), + } + } +} + +pub fn update_widget( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query< + (&Style, &KChildren), + Or<(Changed<Style>, Changed<WidgetProps>, With<Mounted>)>, + >, +) -> bool { + if let Ok((_, children)) = query.get_mut(entity) { + children.process(&widget_context, Some(entity)); + return true; + } + false +} + +fn main() {} diff --git a/examples/windows.rs b/examples/windows.rs deleted file mode 100644 index fe46f1c..0000000 --- a/examples/windows.rs +++ /dev/null @@ -1,57 +0,0 @@ -use bevy::{ - prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}; -use kayak_ui::core::{render, rsx, widget}; -use kayak_ui::widgets::{App, Inspector, Window}; - -#[widget] -fn CustomWidget() { - rsx! { - <> - <Window draggable={true} position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Window 1".to_string()}> - {} - </Window> - <Window draggable={true} position={(550.0, 50.0)} size={(200.0, 200.0)} title={"Window 2".to_string()}> - {} - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <CustomWidget /> - <Inspector /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .run(); -} diff --git a/examples/world_interaction.rs b/examples/world_interaction.rs deleted file mode 100644 index bd2621e..0000000 --- a/examples/world_interaction.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! This example showcases how to handle world interactions in a way that considers Kayak. -//! -//! Specifically, it demonstrates how to determine if a click should affect the world or if -//! it should be left to be handled by the UI. This concept is very important when it comes -//! to designing input handling, as an incorrect implementation could lead to unexpected -//! behavior. - -use bevy::{ - math::{Vec3Swizzles, Vec4Swizzles}, - prelude::{ - App as BevyApp, AssetServer, Camera2dBundle, Color as BevyColor, Commands, Component, - CursorMoved, EventReader, GlobalTransform, Input, MouseButton, Query, Res, ResMut, Sprite, - SpriteBundle, Transform, Vec2, Windows, With, Without, - }, - window::WindowDescriptor, - DefaultPlugins, -}; -use kayak_ui::{ - bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{ - render, rsx, - styles::{Edge, Style, StyleProp, Units}, - use_state, widget, EventType, OnEvent, - }, - widgets::{App, Button, Text, Window}, -}; - -const TILE_SIZE: Vec2 = Vec2::from_array([50.0, 50.0]); -const COLORS: &[BevyColor] = &[BevyColor::TEAL, BevyColor::MAROON, BevyColor::INDIGO]; - -/// This is the system that sets the active tile's target position -/// -/// To prevent the tile from being moved to a position under our UI, we can use the `BevyContext` resource -/// to filter out clicks that occur over the UI -fn set_active_tile_target( - mut tile: Query<&mut ActiveTile>, - cursor: Res<Input<MouseButton>>, - context: Res<BevyContext>, - camera_transform: Query<&GlobalTransform, With<WorldCamera>>, - windows: Res<Windows>, -) { - if !cursor.just_pressed(MouseButton::Left) { - // Only run this system when the mouse button is clicked - return; - } - - if context.contains_cursor() { - // This is the important bit: - // If the cursor is over a part of the UI, then we should not allow clicks to pass through to the world - return; - } - - // If you wanted to allow clicks through the UI as long as the cursor is not on a focusable widget (such as Buttons), - // you could use `context.wants_cursor()` instead: - // - // ``` - // if context.wants_cursor() { - // return; - // } - // ``` - - let world_pos = cursor_to_world(&windows, &camera_transform.single()); - let tile_pos = world_to_tile(world_pos); - let mut tile = tile.single_mut(); - tile.target = tile_pos; -} - -#[widget] -fn ControlPanel() { - let text_styles = Style { - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - let button_styles = Style { - min_width: StyleProp::Value(Units::Pixels(150.0)), - width: StyleProp::Value(Units::Auto), - height: StyleProp::Value(Units::Auto), - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - top: StyleProp::Value(Units::Pixels(16.0)), - bottom: StyleProp::Value(Units::Pixels(8.0)), - padding: StyleProp::Value(Edge::axis(Units::Pixels(8.0), Units::Pixels(48.0))), - ..Default::default() - }; - - let (color_index, set_color_index, ..) = use_state!(0); - let current_index = - context.query_world::<Res<ActiveColor>, _, usize>(|active_color| active_color.index); - if color_index != current_index { - context.query_world::<ResMut<ActiveColor>, _, ()>(|mut active_color| { - active_color.index = color_index - }); - } - - let on_change_color = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - // Cycle the color - set_color_index((color_index + 1) % COLORS.len()); - } - _ => {} - }); - - rsx! { - <> - <Window draggable={true} position={(50.0, 50.0)} size={(300.0, 200.0)} title={"Square Mover: The Game".to_string()}> - <Text size={13.0} content={"You can check if the cursor is over the UI or on a focusable widget using the BevyContext resource.".to_string()} styles={Some(text_styles)} /> - <Button on_event={Some(on_change_color)} styles={Some(button_styles)}> - <Text size={16.0} content={"Change Tile Color".to_string()} /> - </Button> - <Text size={11.0} content={"Go ahead and click the button! The tile won't move.".to_string()} styles={Some(text_styles)} /> - </Window> - </> - } -} - -fn startup( - mut commands: Commands, - mut font_mapping: ResMut<FontMapping>, - asset_server: Res<AssetServer>, -) { - commands.spawn_bundle(UICameraBundle::new()); - - font_mapping.set_default(asset_server.load("roboto.kayak_font")); - - let context = BevyContext::new(|context| { - render! { - <App> - <ControlPanel /> - </App> - } - }); - - commands.insert_resource(context); -} - -fn main() { - BevyApp::new() - .insert_resource(WindowDescriptor { - width: 1270.0, - height: 720.0, - title: String::from("UI Example"), - resizable: false, - ..Default::default() - }) - .insert_resource(ActiveColor { index: 0 }) - .add_plugins(DefaultPlugins) - .add_plugin(BevyKayakUIPlugin) - .add_startup_system(startup) - .add_startup_system(world_setup) - .add_system(move_ghost_tile) - .add_system(set_active_tile_target) - .add_system(move_active_tile) - .add_system(on_color_change) - .run(); -} - -// ! === Unnecessary Details Below === ! // -// Below this point are mainly implementation details. The main purpose of this example is to show how to know -// when to allow or disallow world interaction through `BevyContext` (see the `set_active_tile_target` function) - -/// A resource used to control the color of the tiles -struct ActiveColor { - index: usize, -} - -/// A component used to control the "Active Tile" that moves to the clicked positions -#[derive(Default, Component)] -struct ActiveTile { - target: Vec2, -} - -/// A component used to control the "Ghost Tile" that follows the user's cursor -#[derive(Component)] -struct GhostTile; - -/// A component used to mark the "world camera" (differentiating it from other cameras possibly in the scene) -#[derive(Component)] -struct WorldCamera; - -/// A system that moves the active tile to its target position -fn move_active_tile(mut tile: Query<(&mut Transform, &ActiveTile)>) { - let (mut transform, tile) = tile.single_mut(); - let curr_pos = transform.translation.xy(); - let next_pos = curr_pos.lerp(tile.target, 0.1); - transform.translation.x = next_pos.x; - transform.translation.y = next_pos.y; -} - -/// A system that moves the ghost tile to the cursor's position -fn move_ghost_tile( - mut tile: Query<&mut Transform, With<GhostTile>>, - mut cursor_moved: EventReader<CursorMoved>, - camera_transform: Query<&GlobalTransform, With<WorldCamera>>, - windows: Res<Windows>, -) { - for _ in cursor_moved.iter() { - let world_pos = cursor_to_world(&windows, &camera_transform.single()); - let tile_pos = world_to_tile(world_pos); - let mut ghost = tile.single_mut(); - ghost.translation.x = tile_pos.x; - ghost.translation.y = tile_pos.y; - } -} - -/// A system that updates the tiles' color -fn on_color_change( - mut active_tile: Query<&mut Sprite, (With<ActiveTile>, Without<GhostTile>)>, - mut ghost_tile: Query<&mut Sprite, (With<GhostTile>, Without<ActiveTile>)>, - active_color: Res<ActiveColor>, -) { - if !active_color.is_changed() { - return; - } - - let mut active_tile = active_tile.single_mut(); - active_tile.color = COLORS[active_color.index]; - - let mut ghost_tile = ghost_tile.single_mut(); - ghost_tile.color = ghost_color(COLORS[active_color.index]); -} - -/// A system that sets up the world -fn world_setup(mut commands: Commands, active_color: Res<ActiveColor>) { - commands - .spawn_bundle(Camera2dBundle::default()) - .insert(WorldCamera); - commands - .spawn_bundle(SpriteBundle { - sprite: Sprite { - color: COLORS[active_color.index], - custom_size: Some(TILE_SIZE), - ..Default::default() - }, - ..Default::default() - }) - .insert(ActiveTile::default()); - commands - .spawn_bundle(SpriteBundle { - sprite: Sprite { - color: ghost_color(COLORS[active_color.index]), - custom_size: Some(TILE_SIZE), - ..Default::default() - }, - ..Default::default() - }) - .insert(GhostTile); -} - -/// Get the world position of the cursor in 2D space -fn cursor_to_world(windows: &Windows, camera_transform: &GlobalTransform) -> Vec2 { - let window = windows.get_primary().unwrap(); - let size = Vec2::new(window.width(), window.height()); - - let mut pos = window.cursor_position().unwrap_or_default(); - pos -= size / 2.0; - - let point = camera_transform.compute_matrix() * pos.extend(0.0).extend(1.0); - point.xy() -} - -/// Converts a world coordinate to a rounded tile coordinate -fn world_to_tile(world_pos: Vec2) -> Vec2 { - let extents = TILE_SIZE / 2.0; - let world_pos = world_pos - extents; - (world_pos / TILE_SIZE).ceil() * TILE_SIZE -} - -/// Get the ghost tile color for a given color -fn ghost_color(color: BevyColor) -> BevyColor { - let mut c = color; - c.set_a(0.35); - c -} diff --git a/kayak_core/Cargo.toml b/kayak_core/Cargo.toml deleted file mode 100644 index f79be47..0000000 --- a/kayak_core/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "kayak_core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[features] -default = [] -bevy_renderer = ["bevy", "kayak_font/bevy_renderer"] - -[dependencies] -as-any = "0.2" -desync = { version = "0.7" } -flo_rope = { version = "0.1" } -futures = { version = "0.3" } -kayak_font = { path = "../kayak_font" } -kayak_render_macros = { path = "../kayak_render_macros" } -morphorm = { git = "https://github.com/geom3trik/morphorm", rev = "1243152d4cebea46fd3e5098df26402c73acae91" } -resources = "1.1" -indexmap = "1.8" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -uuid = { version = "0.8", features = ["v4"] } -[target.'cfg(target_arch = "wasm32")'.dependencies] -uuid = { version = "0.8", features = ["v4", "wasm-bindgen"] } - -[dependencies.bevy] -version = "0.8.0" -optional = true -default-features = false \ No newline at end of file diff --git a/kayak_core/src/assets.rs b/kayak_core/src/assets.rs deleted file mode 100644 index b55bc73..0000000 --- a/kayak_core/src/assets.rs +++ /dev/null @@ -1,96 +0,0 @@ -use resources::RefMut; -use std::{collections::HashMap, path::PathBuf}; - -use crate::{Binding, MutableBound}; - -// TODO: Consider aliasing Binding<Option<T>> to be more ergonomic (or maybe use a wrapper struct) -// pub type AssetRef<T> = Binding<Option<T>>; - -pub struct AssetStorage<T> { - assets: HashMap<PathBuf, Binding<Option<T>>>, -} - -impl<T: Clone + PartialEq + Send + Sync + 'static> AssetStorage<T> { - pub fn new() -> Self { - Self { - assets: HashMap::new(), - } - } - - pub fn get(&mut self, key: impl Into<PathBuf>) -> &Binding<Option<T>> { - let key = key.into(); - if self.assets.contains_key(&key) { - self.assets.get(&key).unwrap() - } else { - // Insert new asset if it doesn't exist yet. - self.assets.insert(key.clone(), Binding::new(None)); - self.assets.get(&key).unwrap() - } - } - - pub fn set(&mut self, key: impl Into<PathBuf>, asset: T) { - let key = key.into(); - if self.assets.contains_key(&key) { - let stored_asset = self.assets.get(&key).unwrap(); - stored_asset.set(Some(asset)); - } else { - self.assets.insert(key, Binding::new(Some(asset))); - } - } -} - -/// A collection for storing assets in Kayak -/// -/// This handles getting and setting assets in such a way as to allow them to -/// be bindable by widgets. -#[derive(Default)] -pub struct Assets { - assets: resources::Resources, -} - -impl Assets { - /// Get a stored asset with the given asset key - /// - /// The type of the asset [T] must implement `Clone` and `PartialEq` so that a `Binding<Option<T>>` - /// can be returned. By calling [bind](Self::bind) over the binding, you can react to all changes to - /// the asset, including when it's added or removed. - /// - /// If no asset in storage matches both the asset key _and_ the asset type, a value of - /// `Binding<None>` is returned. Again, binding to this value will allow you to detect when a matching - /// asset is added to storage. - /// - /// # Arguments - /// - /// * `key`: The asset key - /// - pub fn get_asset<T: 'static + Send + Sync + Clone + PartialEq, K: Into<PathBuf>>( - &mut self, - key: K, - ) -> Binding<Option<T>> { - let mut asset_storage = self.get_mut::<T>(); - asset_storage.get(key).clone() - } - - /// Stores an asset along with a key to access it - /// - /// # Arguments - /// - /// * `key`: The asset key - /// * `asset`: The asset to store - /// - pub fn set_asset<T: 'static + Send + Sync + Clone + PartialEq, K: Into<PathBuf>>( - &mut self, - key: K, - asset: T, - ) { - let mut asset_storage = self.get_mut::<T>(); - asset_storage.set(key, asset); - } - - /// Get a mutable reference to the asset storage - fn get_mut<T: 'static + Send + Sync + Clone + PartialEq>(&mut self) -> RefMut<AssetStorage<T>> { - self.assets - .entry() - .or_insert_with(|| AssetStorage::<T>::new()) - } -} diff --git a/kayak_core/src/binding.rs b/kayak_core/src/binding.rs deleted file mode 100644 index 77e9535..0000000 --- a/kayak_core/src/binding.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::time::Instant; - -pub use crate::flo_binding::{ - bind, computed, notify, Binding, Bound, Changeable, ComputedBinding, MutableBound, Releasable, -}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Debouncer { - last_updated: Instant, - threshold: f32, -} - -impl Debouncer { - pub fn new(threshold: f32) -> Self { - Self { - threshold, - last_updated: Instant::now(), - } - } - - pub fn should_update(&mut self) -> bool { - let elapsed_time = self.last_updated.elapsed().as_secs_f32(); - if elapsed_time > self.threshold { - self.last_updated = Instant::now(); - - return true; - } - - return false; - } -} diff --git a/kayak_core/src/children.rs b/kayak_core/src/children.rs deleted file mode 100644 index 7388874..0000000 --- a/kayak_core/src/children.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{Index, KayakContextRef}; -use std::fmt::{Debug, Formatter}; -use std::sync::Arc; - -/// A container for a function that generates child widgets -#[derive(Clone)] -pub struct Children(Arc<dyn Fn(Option<Index>, &mut KayakContextRef) + Send + Sync>); - -impl Children { - pub fn new<F: Fn(Option<Index>, &mut KayakContextRef) + Send + Sync + 'static>( - builder: F, - ) -> Self { - Self(Arc::new(builder)) - } - pub fn build(&self, id: Option<Index>, context: &mut KayakContextRef) { - self.0(id, context); - } -} - -impl Debug for Children { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Children").finish() - } -} - -impl PartialEq for Children { - fn eq(&self, _: &Self) -> bool { - // Never prevent "==" for being true because of this struct - true - } -} diff --git a/kayak_core/src/color.rs b/kayak_core/src/color.rs deleted file mode 100644 index b71a107..0000000 --- a/kayak_core/src/color.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// A color in the sRGB color space. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Color { - /// Red component, 0.0 - 1.0 - pub r: f32, - /// Green component, 0.0 - 1.0 - pub g: f32, - /// Blue component, 0.0 - 1.0 - pub b: f32, - /// Transparency, 0.0 - 1.0 - pub a: f32, -} - -impl Default for Color { - fn default() -> Self { - Self::WHITE - } -} - -impl Color { - /// The black color. - pub const BLACK: Color = Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - - /// The white color. - pub const WHITE: Color = Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }; - - /// A color with no opacity. - pub const TRANSPARENT: Color = Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }; - - pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - Self { r, g, b, a } - } -} diff --git a/kayak_core/src/context.rs b/kayak_core/src/context.rs deleted file mode 100644 index d519cf6..0000000 --- a/kayak_core/src/context.rs +++ /dev/null @@ -1,821 +0,0 @@ -use crate::assets::Assets; -use crate::layout_dispatcher::LayoutEventDispatcher; -use crate::{Binding, Changeable, CursorIcon, KayakContextRef}; -use std::collections::HashMap; -use std::path::PathBuf; - -use crate::event_dispatcher::EventDispatcher; -use crate::{ - multi_state::MultiState, widget_manager::WidgetManager, Index, InputEvent, MutableBound, - Releasable, -}; - -/// The context in which all widgets are contained -/// -/// This manages everything from rendering widgets to processing events. -/// -/// Generally widgets themselves do not need to interface with this struct directly -/// and can instead use the [`KayakContextRef`] abstraction layer. Integrations, on -/// the other hand, will likely need to work with this struct directly so they can -/// control when to render, dispatch events, load assets, etc. -pub struct KayakContext { - assets: Assets, - pub(crate) current_effect_index: usize, - pub(crate) current_state_index: usize, - /// Processes and dispatches all events - event_dispatcher: EventDispatcher, - global_bindings: HashMap<crate::Index, Vec<crate::flo_binding::Uuid>>, - global_state: resources::Resources, - pub(crate) last_state_type_id: Option<std::any::TypeId>, - // TODO: Make widget_manager private. - /// The widget manager containing information about the widget tree and layout - /// - /// # Important Note - /// - /// While this is currently publicly accessible, it's recommended you __don't__ use it - /// within your own code. This will likely be privatized in the future and only - /// accessible through controlled layers of abstraction. - pub widget_manager: WidgetManager, - widget_effects: HashMap<crate::Index, resources::Resources>, - /// Contains provider state data to be accessed by consumers. - /// - /// Maps the type of the data to a mapping of the provider node's ID to the state data - widget_providers: HashMap<std::any::TypeId, HashMap<crate::Index, resources::Resources>>, - widget_state_lifetimes: - HashMap<crate::Index, HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>>, - widget_states: HashMap<crate::Index, resources::Resources>, - cursor_icon: CursorIcon, -} - -impl std::fmt::Debug for KayakContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KayakContext").finish() - } -} - -impl KayakContext { - /// Creates a new [`KayakContext`]. - pub fn new() -> Self { - Self { - assets: Assets::default(), - current_effect_index: 0, - current_state_index: 0, - cursor_icon: CursorIcon::Default, - event_dispatcher: EventDispatcher::new(), - global_bindings: HashMap::new(), - global_state: resources::Resources::default(), - last_state_type_id: None, - widget_effects: HashMap::new(), - widget_manager: WidgetManager::new(), - widget_providers: HashMap::new(), - widget_state_lifetimes: HashMap::new(), - widget_states: HashMap::new(), - } - } - - /// Bind the given widget to a `Binding<T>` value - /// - /// "Binding" means that whenever the bound value is changed, the given widget will be re-rendered. - /// To undo this effect, use the [`unbind`](Self::unbind) method. - /// - /// Make sure the binding is stored _outside_ the widget's scope. Otherwise, it will just be dropped - /// once the widget is rendered. - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// * `binding`: The value to bind to - /// - pub fn bind<T: Clone + PartialEq + Send + Sync + 'static>( - &mut self, - widget_id: Index, - binding: &crate::Binding<T>, - ) { - if !self.global_bindings.contains_key(&widget_id) { - self.global_bindings.insert(widget_id, vec![]); - } - - let global_binding_ids = self.global_bindings.get_mut(&widget_id).unwrap(); - if !global_binding_ids.contains(&binding.id) { - let lifetime = Self::create_lifetime(&binding, &self.widget_manager, widget_id); - Self::insert_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - binding.id, - lifetime, - ); - global_binding_ids.push(binding.id); - } - } - - /// Unbinds the given widget from a `Binding<T>` value - /// - /// The will only work on values for which the given widget has already been bound - /// using the [`bind`](Self::bind) method. - /// - /// If the given value was not already bound, this method does nothing. - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// * `binding`: The already-bound value - /// - pub fn unbind<T: Clone + PartialEq + Send + Sync + 'static>( - &mut self, - widget_id: Index, - binding: &crate::Binding<T>, - ) { - if self.global_bindings.contains_key(&widget_id) { - let global_binding_ids = self.global_bindings.get_mut(&widget_id).unwrap(); - if let Some(index) = global_binding_ids.iter().position(|id| *id == binding.id) { - global_binding_ids.remove(index); - - Self::remove_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - binding.id, - ); - } - } - } - - /// Creates a provider context with the given state data - /// - /// This works much like [create_state](Self::create_state), except that the state is also made available to any - /// descendent widget. They can access this provider's state by calling [create_consumer](Self::create_consumer). - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// * `initial_state`: The initial value to set (if it hasn't been set already) - /// - pub fn create_provider<T: resources::Resource + Clone + PartialEq>( - &mut self, - widget_id: Index, - initial_state: T, - ) -> Binding<T> { - let type_id = initial_state.type_id(); - - let providers = self - .widget_providers - .entry(type_id.clone()) - .or_insert(HashMap::default()); - - if let Some(provider) = providers.get(&widget_id) { - if let Ok(state) = provider.get::<Binding<T>>() { - // Provider was already created - return state.clone(); - } - } - - let mut provider = resources::Resources::default(); - let state = crate::bind(initial_state); - let lifetime = Self::create_lifetime(&state, &self.widget_manager, widget_id); - Self::insert_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - state.id, - lifetime, - ); - provider.insert(state.clone()); - providers.insert(widget_id, provider); - - state - } - - /// Creates a context consumer for the given type, [T] - /// - /// This allows direct access to a parent's state data made with [create_provider](Self::create_provider). - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// - pub fn create_consumer<T: resources::Resource + Clone + PartialEq>( - &mut self, - widget_id: Index, - ) -> Option<Binding<T>> { - let type_id = std::any::TypeId::of::<T>(); - - if let Some(providers) = self.widget_providers.get(&type_id) { - let mut index = Some(widget_id); - while index.is_some() { - // Traverse the parents to find the one with the given state data - index = self.widget_manager.tree.get_parent(index.unwrap()); - - if let Some(key) = index { - if let Some(provider) = providers.get(&key) { - if let Ok(state) = provider.get::<Binding<T>>() { - return Some(state.clone()); - } - } - } - } - } - - None - } - - /// Create a state - /// - /// A "state" is a value that is maintained across re-renders of a widget. Additionally, widgets - /// are _bound_ to their state. This means that whenever the state is updated, it will cause the - /// widget to re-render. - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// * `initial_state`: The initial value to set (if it hasn't been set already) - /// - /// # Examples - /// - /// Creating a state is easy. With the `Bound` and `MutableBound` traits in scope, we can then - /// `get` and `set` the state value, respectively. - /// - /// ```ignore - /// #[widget] - /// fn MyWidget() { - /// // Create state - /// let count = context.create_state::<u32>(0); - /// - /// // Get current value - /// let count_value = count.get(); - /// - /// // Set value (this would cause the a re-render, resulting in an infinite loop) - /// count.set(count_value + 1); - /// } - /// ``` - /// - /// The order in which states are defined matters. Placing this method behind some type of conditional - /// can lead to unexpected behavior, such as one state being set to the value of another state. - /// - /// ```should_panic - /// #[widget] - /// fn MyWidget() { - /// let some_conditional = context.create_state(true); - /// - /// if some_conditional { - /// let count_a = context.create_state::<u32>(123); - /// some_conditional.set(false); - /// } - /// - /// let count_b = context.create_state::<u32>(0); - /// - /// assert_eq!(0, count_b.get()); - /// } - /// ``` - pub fn create_state<T: resources::Resource + Clone + PartialEq>( - &mut self, - widget_id: Index, - initial_state: T, - ) -> Option<crate::Binding<T>> { - let state_type_id = initial_state.type_id(); - if let Some(last_state_type_id) = self.last_state_type_id { - if state_type_id != last_state_type_id { - self.current_state_index = 0; - } - } - - if self.widget_states.contains_key(&widget_id) { - let states = self.widget_states.get_mut(&widget_id).unwrap(); - if !states.contains::<MultiState<crate::Binding<T>>>() { - let state = crate::bind(initial_state); - let lifetime = Self::create_lifetime(&state, &self.widget_manager, widget_id); - Self::insert_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - state.id, - lifetime, - ); - states.insert(MultiState::new(state)); - self.last_state_type_id = Some(state_type_id); - self.current_state_index += 1; - } else { - // Add new value to the multi-state. - let state = crate::bind(initial_state); - let lifetime = Self::create_lifetime(&state, &self.widget_manager, widget_id); - Self::insert_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - state.id, - lifetime, - ); - let mut multi_state = states.remove::<MultiState<crate::Binding<T>>>().unwrap(); - multi_state.get_or_add(state, &mut self.current_state_index); - states.insert(multi_state); - self.last_state_type_id = Some(state_type_id); - } - } else { - let mut states = resources::Resources::default(); - let state = crate::bind(initial_state); - let lifetime = Self::create_lifetime(&state, &self.widget_manager, widget_id); - Self::insert_state_lifetime( - &mut self.widget_state_lifetimes, - widget_id, - state.id, - lifetime, - ); - states.insert(MultiState::new(state)); - self.widget_states.insert(widget_id, states); - self.current_state_index += 1; - self.last_state_type_id = Some(state_type_id); - } - return self.get_state(widget_id); - } - - /// Creates a callback that runs as a side-effect of one of its dependencies being changed. - /// - /// All dependencies must be implement the [Changeable](crate::Changeable) trait, which means it will generally - /// work best with [Binding](crate::Binding) values, such as those created by [`create_state`](Self::create_state). - /// - /// Use an empty dependency array if you want this effect to run only when the widget is _first_ rendered - /// (then never again). - /// - /// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-effect.html), - /// upon which this method is based. - /// - /// # Arguments - /// - /// * `widget_id`: The ID of the widget - /// * `effect`: The side-effect function - /// * `dependencies`: The dependencies the effect relies on - /// - /// # Examples - /// - /// ```ignore - /// #[widget] - /// fn MyWidget() { - /// let count = context.create_state::<u32>(0); - /// - /// // An effect that prints out the count value whenever it changes - /// context.create_effect(move || { - /// println!("Value: {}", count.get()); - /// }, &[&count]); - /// - /// // An effect that prints to the console when the widget is first rendered - /// context.create_effect(|| { - /// println!("MyWidget created!"); - /// }, &[]); - /// } - /// ``` - pub fn create_effect<'a, F: Fn() + Send + Sync + 'static>( - &'a mut self, - widget_id: Index, - effect: F, - dependencies: &[&'a dyn Changeable], - ) { - // === Bind to Dependencies === // - let notification = crate::notify(effect); - let mut lifetimes = Vec::default(); - for dependency in dependencies { - let lifetime = dependency.when_changed(notification.clone()); - lifetimes.push(lifetime); - } - - // === Create Invoking Function === // - // Create a temporary Binding to allow us to invoke the effect if needed - let notify_clone = notification.clone(); - let invoke_effect = move || { - let control = crate::bind(false); - let mut control_life = control.when_changed(notify_clone.clone()); - control.set(true); - control_life.done(); - }; - - // === Insert Effect === // - let effects = self - .widget_effects - .entry(widget_id) - .or_insert(resources::Resources::default()); - if effects.contains::<MultiState<Vec<Box<dyn Releasable>>>>() { - let mut state = effects - .get_mut::<MultiState<Vec<Box<dyn Releasable>>>>() - .unwrap(); - let old_size = state.data.len(); - state.get_or_add(lifetimes, &mut self.current_effect_index); - if old_size != state.data.len() { - // Just added -> invoke effect - invoke_effect(); - } - } else { - let state = MultiState::new(lifetimes); - effects.insert(state); - invoke_effect(); - self.current_effect_index += 1; - } - } - - fn get_state<T: resources::Resource + Clone + PartialEq>(&self, widget_id: Index) -> Option<T> { - if self.widget_states.contains_key(&widget_id) { - let states = self.widget_states.get(&widget_id).unwrap(); - if let Ok(state) = states.get::<MultiState<T>>() { - return Some(state.get(self.current_state_index - 1).clone()); - } - } - return None; - } - - /// Create a `Releasable` lifetime that marks the current node as dirty when the given state changes - fn create_lifetime<T: resources::Resource + Clone + PartialEq>( - state: &Binding<T>, - widget_manager: &WidgetManager, - id: Index, - ) -> Box<dyn Releasable> { - let dirty_nodes = widget_manager.dirty_nodes.clone(); - state.when_changed(crate::notify(move || { - if let Ok(mut dirty_nodes) = dirty_nodes.lock() { - dirty_nodes.insert(id); - } - })) - } - - fn insert_state_lifetime( - lifetimes: &mut HashMap< - crate::Index, - HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>, - >, - id: Index, - binding_id: crate::flo_binding::Uuid, - lifetime: Box<dyn crate::Releasable>, - ) { - if lifetimes.contains_key(&id) { - if let Some(lifetimes) = lifetimes.get_mut(&id) { - if !lifetimes.contains_key(&binding_id) { - lifetimes.insert(binding_id, lifetime); - } - } - } else { - let mut new_hashmap = HashMap::new(); - new_hashmap.insert(binding_id, lifetime); - lifetimes.insert(id, new_hashmap); - } - } - - fn remove_state_lifetime( - lifetimes: &mut HashMap< - crate::Index, - HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>, - >, - id: Index, - binding_id: crate::flo_binding::Uuid, - ) { - if lifetimes.contains_key(&id) { - if let Some(lifetimes) = lifetimes.get_mut(&id) { - if lifetimes.contains_key(&binding_id) { - let mut binding_lifetime = lifetimes.remove(&binding_id).unwrap(); - binding_lifetime.done(); - } - } - } - } - - /// Set a value that's accessible to all widgets - /// - /// Values should be type-unique. Setting an `i32` value, for example, allows another widget - /// to overwrite that value by adding their own global `i32` value, whether or not it was intentional. - /// If this is not desired, an easy solution is to use the [newtype](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) - /// pattern. - /// - /// Widgets are not automatically bound to this global. You will have to bind to it manually - /// (as long as the value is a `Binding<T>`) using [`bind`](Self::bind). - /// - /// # Arguments - /// - /// * `value`: The value to set - /// - /// # Examples - /// - /// ```ignore - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// context.set_global(MyCount(123)); - /// } - /// ``` - /// - /// You may also want to bind the widget to a global, so that when the global is changed, - /// the widget will re-render. This can be done by binding to the global. - /// - /// ```ignore - /// use kayak_core::bind; - /// - /// #[derive(Clone, PartialEq)] // <- Required by `bind` - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// let bound_count = bind(MyCount(123)); - /// context.bind(&bound_count); - /// context.set_global(bound_count); - /// } - /// ``` - pub fn set_global<T: resources::Resource>(&mut self, value: T) { - self.global_state.insert(value); - } - - /// Attempts to fetch a global value with the given type, returning an immutable reference to - /// that value. - /// - /// If you need mutable access to the global, use the [`get_global_mut`](Self::get_global_mut) method. - pub fn get_global<T: resources::Resource>( - &mut self, - ) -> Result<resources::Ref<T>, resources::CantGetResource> { - self.global_state.get::<T>() - } - - /// Attempts to fetch a global value with the given type, returning a mutable reference to - /// that value. - /// - /// If you only need immutable access to the global, use the [`get_global`](Self::get_global) method. - pub fn get_global_mut<T: resources::Resource>( - &mut self, - ) -> Result<resources::RefMut<T>, resources::CantGetResource> { - self.global_state.get_mut::<T>() - } - - /// Removes the global value with the given type - /// - /// Returns the removed value, or `None` if a value with the given type did not exist. - pub fn remove_global<T: resources::Resource>(&mut self) -> Option<T> { - self.global_state.remove::<T>() - } - - /// Re-render all widgets that need rendering (i.e., marked dirty) - pub fn render(&mut self) { - let dirty_nodes: Vec<_> = - if let Ok(mut dirty_nodes) = self.widget_manager.dirty_nodes.lock() { - dirty_nodes.drain(..).collect() - } else { - panic!("Couldn't get lock on dirty nodes!") - }; - for node_index in dirty_nodes { - let mut widget = self.widget_manager.take(node_index); - let mut context = KayakContextRef::new(self, Some(node_index)); - widget.render(&mut context); - self.widget_manager.repossess(widget); - self.widget_manager.dirty_render_nodes.insert(node_index); - } - - // self.widget_manager.dirty_nodes.clear(); - self.widget_manager.render(&mut self.assets); - LayoutEventDispatcher::dispatch(self); - self.update_cursor(); - } - - /// Processes the given input events - /// - /// Events are processed in three phases: Capture, Target, Propagate. These phases are based on their - /// associated [W3 specifications](https://www.w3.org/TR/uievents/#dom-event-architecture). - /// - /// ## Capture: - /// Currently, we do not support the Capture Phase. This is because the current event handling system is - /// made to handle events as a single enum. To achieve proper capturing, widgets would need to be able to - /// register separate event handlers so that specific ones could be captured while others would not. It - /// should generally be okay to skip this as it's not a common use-case. - /// - /// ## Target: - /// The Target Phase simply identifies the target for an event so that we can generate the propagation path - /// for it. - /// - /// ## Propagate: - /// The Propagate Phase (also known as the Bubble Phase) is where we bubble up the tree from the target node, - /// firing the bubbled event along the way. At any point, the bubbling can be stopped by calling - /// [`event.stop_propagation()`](Event::stop_propagation). Not every event can be propagated, in which case, - /// they will only fire for their specified target. - pub fn process_events(&mut self, input_events: Vec<InputEvent>) { - let mut dispatcher = self.event_dispatcher.to_owned(); - dispatcher.process_events(input_events, self); - self.event_dispatcher.merge(dispatcher); - } - - #[allow(dead_code)] - fn get_all_parents(&self, current: Index, parents: &mut Vec<Index>) { - if let Some(parent) = self.widget_manager.tree.parents.get(¤t) { - parents.push(*parent); - self.get_all_parents(*parent, parents); - } - } - - /// Checks if the widget with the given ID is currently focused or not - pub fn is_focused(&self, index: Index) -> bool { - let current = self.widget_manager.focus_tree.current(); - current == Some(index) - } - - /// Gets the currently focused widget ID - pub fn current_focus(&self) -> Option<Index> { - self.widget_manager.focus_tree.current() - } - - /// Gets whether the widget with the given ID can be focused - /// - /// The values are: - /// - /// | Value | Description | - /// |---------------|------------------------------------------| - /// | `Some(true)` | The widget is focusable | - /// | `Some(false)` | The widget is not focusable | - /// | `None` | The widget's focusability is unspecified | - /// - pub fn get_focusable(&self, index: Index) -> Option<bool> { - self.widget_manager.get_focusable(index) - } - - /// Sets the "focusability" of the widget with the given ID - /// - /// The values are: - /// - /// | Value | Description | - /// |---------------|------------------------------------------| - /// | `Some(true)` | The widget is focusable | - /// | `Some(false)` | The widget is not focusable | - /// | `None` | The widget's focusability is unspecified | - /// - pub fn set_focusable(&mut self, focusable: Option<bool>, index: Index) { - self.widget_manager.set_focusable(focusable, index, false); - } - - /// Get the last calculated mouse position. - /// - /// Calling this from a widget will return the last mouse position at the time the widget was rendered. - pub fn last_mouse_position(&self) -> (f32, f32) { - self.event_dispatcher.current_mouse_position() - } - - /// Query the Bevy `World` with the given `SystemParam` - /// - /// The function passed to this method will be called with the retrieved value from `World`. If - /// a value is returned from that function, it will be returned from this method as well. - /// - /// # Arguments - /// - /// * `f`: The function to call with the given system parameter - /// - /// # Examples - /// - /// ```ignore - /// use bevy::prelude::{Query, Res, Transform}; - /// - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// // Query a single item - /// let value = context.query_world::<Res<MyCount>, _, _>(|count| count.0); - /// - /// // Or query multiple using a tuple - /// context.query_world::<(Res<MyCount>, Query<&mut Transform>), _, _>(|(count, query)| { - /// // ... - /// }); - /// } - /// ``` - #[cfg(feature = "bevy_renderer")] - pub fn query_world<T: bevy::ecs::system::SystemParam, F, R>(&mut self, mut f: F) -> R - where - F: FnMut(<T::Fetch as bevy::ecs::system::SystemParamFetch<'_, '_>>::Item) -> R, - { - let mut world = self.get_global_mut::<bevy::prelude::World>().unwrap(); - let mut system_state = bevy::ecs::system::SystemState::<T>::new(&mut world); - let r = { - let test = system_state.get_mut(&mut world); - f(test) - }; - system_state.apply(&mut world); - - r - } - - /// Get a stored asset with the given asset key - /// - /// The type of the asset [T] must implement `Clone` and `PartialEq` so that a `Binding<Option<T>>` - /// can be returned. By calling [bind](Self::bind) over the binding, you can react to all changes to - /// the asset, including when it's added or removed. - /// - /// If no asset in storage matches both the asset key _and_ the asset type, a value of - /// `Binding<None>` is returned. Again, binding to this value will allow you to detect when a matching - /// asset is added to storage. - /// - /// # Arguments - /// - /// * `key`: The asset key - /// - /// # Examples - /// - /// ```ignore - /// #[derive(Clone, PartialEq)] - /// struct MyAsset(pub String); - /// - /// #[widget] - /// fn MyWidget() { - /// let asset = context.get_asset::<MyAsset>("foo"); - /// context.bind(&asset); - /// if let Some(asset) = asset.get() { - /// // ... - /// } - /// } - /// ``` - pub fn get_asset<T: 'static + Send + Sync + Clone + PartialEq>( - &mut self, - key: impl Into<PathBuf>, - ) -> Binding<Option<T>> { - self.assets.get_asset(key) - } - - /// Stores an asset along with a key to access it - /// - /// # Arguments - /// - /// * `key`: The asset key - /// * `asset`: The asset to store - /// - pub fn set_asset<T: 'static + Send + Sync + Clone + PartialEq>( - &mut self, - key: impl Into<PathBuf>, - asset: T, - ) { - self.assets.set_asset(key, asset); - } - - /// Get the ID of the widget that was last clicked - pub fn get_last_clicked_widget(&self) -> Binding<Index> { - self.event_dispatcher.last_clicked.clone() - } - - /// Returns true if the cursor is currently over a valid widget - /// - /// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own. - /// This means widgets specified with `RenderCommand::Empty`, `RenderCommand::Layout`, or `RenderCommand::Clip` - /// do not meet the requirements to "contain" the cursor. - pub fn contains_cursor(&self) -> bool { - self.event_dispatcher.contains_cursor() - } - - /// Returns true if the cursor may be needed by a widget or it's already in use by one - /// - /// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets - /// include buttons, sliders, and text boxes. - pub fn wants_cursor(&self) -> bool { - self.event_dispatcher.wants_cursor() - } - - /// Returns true if the cursor is currently in use by a widget - /// - /// This is most often useful for checking drag events as it will still return true even if the drag continues outside - /// the widget bounds (as long as it started within it). - pub fn has_cursor(&self) -> bool { - self.event_dispatcher.has_cursor() - } - - /// Captures all cursor events and instead makes the given index the target - pub fn capture_cursor(&mut self, index: Index) -> Option<Index> { - self.event_dispatcher.capture_cursor(index) - } - - /// Releases the captured cursor - /// - /// Returns true if successful. - /// - /// This will only release the cursor if the given index matches the current captor. This - /// prevents other widgets from accidentally releasing against the will of the original captor. - /// - /// This check can be side-stepped if necessary by calling [`force_release_cursor`](Self::force_release_cursor) - /// instead (or by calling this method with the correct index). - pub fn release_cursor(&mut self, index: Index) -> bool { - self.event_dispatcher.release_cursor(index) - } - - /// Releases the captured cursor - /// - /// Returns the index of the previous captor. - /// - /// This will force the release, regardless of which widget has called it. To safely release, - /// use the standard [`release_cursor`](Self::release_cursor) method instead. - pub fn force_release_cursor(&mut self) -> Option<Index> { - self.event_dispatcher.force_release_cursor() - } - - /// Get the current cursor icon - pub fn cursor_icon(&self) -> CursorIcon { - self.cursor_icon - } - - #[allow(dead_code)] - pub(crate) fn set_cursor_icon(&mut self, icon: CursorIcon) { - self.cursor_icon = icon; - } - - fn update_cursor(&mut self) { - if self.event_dispatcher.hovered.is_none() { - return; - } - - let hovered = self.event_dispatcher.hovered.unwrap(); - if let Some(node) = self.widget_manager.nodes.get(hovered) { - if let Some(node) = node { - let icon = node.resolved_styles.cursor.resolve(); - self.cursor_icon = icon; - } - } - } -} diff --git a/kayak_core/src/context_ref.rs b/kayak_core/src/context_ref.rs deleted file mode 100644 index b543fd0..0000000 --- a/kayak_core/src/context_ref.rs +++ /dev/null @@ -1,586 +0,0 @@ -use std::path::PathBuf; - -use crate::{Binding, Changeable, Index, KayakContext, WidgetTree}; - -/// A temporary struct used to provide limited access to the containing [`KayakContext`] -/// -/// This provides a safer, cleaner way to access `KayakContext` when working with widgets. -pub struct KayakContextRef<'a> { - /// A reference to the actual [`KayakContext`] - pub(crate) context: &'a mut KayakContext, - /// The ID of the current widget (usually the one this was passed to) - current_id: Option<Index>, - /// The currently generated widget tree - tree: Option<WidgetTree>, -} - -impl<'a> KayakContextRef<'a> { - /// Creates a new `KayakContextRef` - /// - /// # Arguments - /// - /// * `context`: The containing `KayakContext` - /// * `current_id`: The Id of the current widget - /// - pub fn new(context: &'a mut KayakContext, current_id: Option<Index>) -> Self { - // TODO: Change this so that KayakContextRef keeps track of these instead of the KayakContext. - context.last_state_type_id = None; - context.current_state_index = 0; - context.current_effect_index = 0; - Self { - context, - current_id, - tree: Some(WidgetTree::new()), - } - } - - /// Bind this widget to a `Binding<T>` value - /// - /// "Binding" means that whenever the bound value is changed, the current widget will be re-rendered. - /// To undo this effect, use the [`unbind`](Self::unbind) method. - /// - /// Make sure the binding is stored _outside_ the widget's scope. Otherwise, it will just be dropped - /// once the widget is rendered. - /// - /// # Arguments - /// - /// * `binding`: The value to bind to - /// - pub fn bind<T: Clone + PartialEq + Send + Sync + 'static>(&mut self, binding: &Binding<T>) { - self.context - .bind(self.current_id.unwrap_or_default(), binding); - } - - /// Unbinds the current widget from a `Binding<T>` value - /// - /// The will only work on values for which the current widget has already been bound - /// using the [`bind`](Self::bind) method. - /// - /// If the given value was not already bound, this method does nothing. - /// - /// # Arguments - /// - /// * `binding`: The already-bound value - /// - pub fn unbind<T: Clone + PartialEq + Send + Sync + 'static>(&mut self, binding: &Binding<T>) { - self.context - .unbind(self.current_id.unwrap_or_default(), binding); - } - - /// Creates a provider context with the given state data - /// - /// This works much like [create_state](Self::create_state), except that the state is also made available to any - /// descendent widget. They can access this provider's state by calling [create_consumer](Self::create_consumer). - /// - /// # Arguments - /// - /// * `initial_state`: The initial value to set (if it hasn't been set already) - /// - pub fn create_provider<T: resources::Resource + Clone + PartialEq>( - &mut self, - initial_state: T, - ) -> Binding<T> { - self.context - .create_provider(self.current_id.unwrap_or_default(), initial_state) - } - - /// Creates a context consumer for the given type, [T] - /// - /// This allows direct access to a parent's state data made with [create_provider](Self::create_provider). - pub fn create_consumer<T: resources::Resource + Clone + PartialEq>( - &mut self, - ) -> Option<Binding<T>> { - self.context - .create_consumer(self.current_id.unwrap_or_default()) - } - - /// Create a state - /// - /// A "state" is a value that is maintained across re-renders of a widget. Additionally, widgets - /// are _bound_ to their state. This means that whenever the state is updated, it will cause the - /// widget to re-render. - /// - /// # Arguments - /// - /// * `initial_state`: The initial value to set (if it hasn't been set already) - /// - /// # Examples - /// - /// Creating a state is easy. With the `Bound` and `MutableBound` traits in scope, we can then - /// `get` and `set` the state value, respectively. - /// - /// ```ignore - /// #[widget] - /// fn MyWidget() { - /// // Create state - /// let count = context.create_state::<u32>(0); - /// - /// // Get current value - /// let count_value = count.get(); - /// - /// // Set value (this would cause the a re-render, resulting in an infinite loop) - /// count.set(count_value + 1); - /// } - /// ``` - /// - /// The order in which states are defined matters. Placing this method behind some type of conditional - /// can lead to unexpected behavior, such as one state being set to the value of another state. - /// - /// ```should_panic - /// #[widget] - /// fn MyWidget() { - /// let some_conditional = context.create_state(true); - /// - /// if some_conditional { - /// let count_a = context.create_state::<u32>(123); - /// some_conditional.set(false); - /// } - /// - /// let count_b = context.create_state::<u32>(0); - /// - /// assert_eq!(0, count_b.get()); - /// } - /// ``` - pub fn create_state<T: resources::Resource + Clone + PartialEq>( - &mut self, - initial_state: T, - ) -> Option<crate::Binding<T>> { - self.context - .create_state(self.current_id.unwrap_or_default(), initial_state) - } - - /// Creates a callback that runs as a side-effect of one of its dependencies being changed. - /// - /// All dependencies must be implement the [Changeable](crate::Changeable) trait, which means it will generally - /// work best with [Binding](crate::Binding) values, such as those created by [`create_state`](Self::create_state). - /// - /// Use an empty dependency array if you want this effect to run only when the widget is _first_ rendered - /// (then never again). - /// - /// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-effect.html), - /// upon which this method is based. - /// - /// # Arguments - /// - /// * `effect`: The side-effect function - /// * `dependencies`: The dependencies the effect relies on - /// - /// # Examples - /// - /// ```ignore - /// #[widget] - /// fn MyWidget() { - /// let count = context.create_state::<u32>(0); - /// - /// // An effect that prints out the count value whenever it changes - /// context.create_effect(move || { - /// println!("Value: {}", count.get()); - /// }, &[&count]); - /// - /// // An effect that prints to the console when the widget is first rendered - /// context.create_effect(|| { - /// println!("MyWidget created!"); - /// }, &[]); - /// } - /// ``` - pub fn create_effect<'b, F: Fn() + Send + Sync + 'static>( - &'b mut self, - effect: F, - dependencies: &[&'b dyn Changeable], - ) { - self.context - .create_effect(self.current_id.unwrap_or_default(), effect, dependencies); - } - - /// Set a value that's accessible to all widgets - /// - /// Values should be type-unique. Setting an `i32` value, for example, allows another widget - /// to overwrite that value by adding their own global `i32` value, whether or not it was intentional. - /// If this is not desired, an easy solution is to use the [newtype](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) - /// pattern. - /// - /// Widgets are not automatically bound to this global. You will have to bind to it manually - /// (as long as the value is a `Binding<T>`) using [`bind`](Self::bind). - /// - /// # Arguments - /// - /// * `value`: The value to set - /// - /// # Examples - /// - /// ```ignore - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// context.set_global(MyCount(123)); - /// } - /// ``` - /// - /// You may also want to bind the widget to a global, so that when the global is changed, - /// the widget will re-render. This can be done by binding to the global. - /// - /// ```ignore - /// use kayak_core::bind; - /// - /// #[derive(Clone, PartialEq)] // <- Required by `bind` - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// let bound_count = bind(MyCount(123)); - /// context.bind(&bound_count); - /// context.set_global(bound_count); - /// } - /// ``` - pub fn set_global<T: resources::Resource>(&mut self, value: T) { - self.context.set_global(value); - } - - /// Attempts to fetch a global value with the given type, returning an immutable reference to - /// that value. - /// - /// If you need mutable access to the global, use the [`get_global_mut`](Self::get_global_mut) method. - pub fn get_global<T: resources::Resource>( - &mut self, - ) -> Result<resources::Ref<T>, resources::CantGetResource> { - self.context.get_global() - } - - /// Attempts to fetch a global value with the given type, returning a mutable reference to - /// that value. - /// - /// If you only need immutable access to the global, use the [`get_global`](Self::get_global) method. - pub fn get_global_mut<T: resources::Resource>( - &mut self, - ) -> Result<resources::RefMut<T>, resources::CantGetResource> { - self.context.get_global_mut() - } - - /// Removes the global value with the given type - /// - /// Returns the removed value, or `None` if a value with the given type did not exist. - pub fn remove_global<T: resources::Resource>(&mut self) -> Option<T> { - self.context.remove_global() - } - - /// Checks if the widget with the given ID is currently focused or not - pub fn is_focused(&self, id: Index) -> bool { - self.context.is_focused(id) - } - - /// Gets the currently focused widget ID - pub fn current_focus(&self) -> Option<Index> { - self.context.current_focus() - } - - /// Gets whether the widget with the given ID can be focused - /// - /// The values are: - /// - /// | Value | Description | - /// |---------------|------------------------------------------| - /// | `Some(true)` | The widget is focusable | - /// | `Some(false)` | The widget is not focusable | - /// | `None` | The widget's focusability is unspecified | - /// - pub fn get_focusable(&self, id: Index) -> Option<bool> { - self.context.get_focusable(id) - } - - /// Sets the current widget's "focusability" - /// - /// The values are: - /// - /// | Value | Description | - /// |---------------|------------------------------------------| - /// | `Some(true)` | The widget is focusable | - /// | `Some(false)` | The widget is not focusable | - /// | `None` | The widget's focusability is unspecified | - /// - pub fn set_focusable(&mut self, focusable: Option<bool>) { - if let Some(id) = self.current_id { - self.context.set_focusable(focusable, id); - } - } - - /// Query the Bevy `World` with the given `SystemParam` - /// - /// The function passed to this method will be called with the retrieved value from `World`. If - /// a value is returned from that function, it will be returned from this method as well. - /// - /// # Arguments - /// - /// * `f`: The function to call with the given system parameter - /// - /// # Examples - /// - /// ```ignore - /// use bevy::prelude::{Query, Res, Transform}; - /// - /// struct MyCount(i32); - /// - /// #[widget] - /// fn MyWidget() { - /// // Query a single item - /// let value = context.query_world::<Res<MyCount>, _, _>(|count| count.0); - /// - /// // Or query multiple using a tuple - /// context.query_world::<(Res<MyCount>, Query<&mut Transform>), _, _>(|(count, query)| { - /// // ... - /// }); - /// } - /// ``` - #[cfg(feature = "bevy_renderer")] - pub fn query_world<T: bevy::ecs::system::SystemParam, F, R>(&mut self, f: F) -> R - where - F: FnMut(<T::Fetch as bevy::ecs::system::SystemParamFetch<'_, '_>>::Item) -> R, - { - self.context.query_world::<T, F, R>(f) - } - - /// Get a stored asset with the given asset key - /// - /// The type of the asset [T] must implement `Clone` and `PartialEq` so that a `Binding<Option<T>>` - /// can be returned. By calling [bind](Self::bind) over the binding, you can react to all changes to - /// the asset, including when it's added or removed. - /// - /// If no asset in storage matches both the asset key _and_ the asset type, a value of - /// `Binding<None>` is returned. Again, binding to this value will allow you to detect when a matching - /// asset is added to storage. - /// - /// # Arguments - /// - /// * `key`: The asset key - /// - /// # Examples - /// - /// ```ignore - /// # #[derive(Clone, PartialEq)] - /// # struct MyAsset(pub String); - /// - /// #[widget] - /// fn MyWidget() { - /// let asset = context.get_asset::<MyAsset>("foo"); - /// context.bind(&asset); - /// if let Some(asset) = asset.get() { - /// // ... - /// } - /// } - /// ``` - pub fn get_asset<T: 'static + Send + Sync + Clone + PartialEq>( - &mut self, - key: impl Into<PathBuf>, - ) -> Binding<Option<T>> { - self.context.get_asset(key) - } - - /// Stores an asset along with a key to access it - /// - /// # Arguments - /// - /// * `key`: The asset key - /// * `asset`: The asset to store - /// - pub fn set_asset<T: 'static + Send + Sync + Clone + PartialEq>( - &mut self, - key: impl Into<PathBuf>, - asset: T, - ) { - self.context.set_asset(key, asset) - } - - /// Get the last calculated mouse position. - /// - /// Calling this from a widget will return the last mouse position at the time the widget was rendered. - pub fn last_mouse_position(&self) -> (f32, f32) { - self.context.last_mouse_position() - } - - /// Get the ID of the widget that was last clicked - pub fn get_last_clicked_widget(&self) -> Binding<Index> { - self.context.get_last_clicked_widget() - } - - /// Returns true if the cursor is currently over a valid widget - /// - /// For the purposes of this method, a valid widget is one which has the means to display a visual component on its own. - /// This means widgets specified with `RenderCommand::Empty`, `RenderCommand::Layout`, or `RenderCommand::Clip` - /// do not meet the requirements to "contain" the cursor. - pub fn contains_cursor(&self) -> bool { - self.context.contains_cursor() - } - - /// Returns true if the cursor may be needed by a widget or it's already in use by one - /// - /// This is useful for checking if certain events (such as a click) would "matter" to the UI at all. Example widgets - /// include buttons, sliders, and text boxes. - pub fn wants_cursor(&self) -> bool { - self.context.wants_cursor() - } - - /// Returns true if the cursor is currently in use by a widget - /// - /// This is most often useful for checking drag events as it will still return true even if the drag continues outside - /// the widget bounds (as long as it started within it). - pub fn has_cursor(&self) -> bool { - self.context.has_cursor() - } - - /// Captures all cursor events and instead makes the given index the target - pub fn capture_cursor(&mut self, index: Index) -> Option<Index> { - self.context.capture_cursor(index) - } - - /// Releases the captured cursor - /// - /// Returns true if successful. - /// - /// This will only release the cursor if the given index matches the current captor. This - /// prevents other widgets from accidentally releasing against the will of the original captor. - /// - /// This check can be side-stepped if necessary by calling [`force_release_cursor`](Self::force_release_cursor) - /// instead (or by calling this method with the correct index). - pub fn release_cursor(&mut self, index: Index) -> bool { - self.context.release_cursor(index) - } - - /// Releases the captured cursor - /// - /// Returns the index of the previous captor. - /// - /// This will force the release, regardless of which widget has called it. To safely release, - /// use the standard [`release_cursor`](Self::release_cursor) method instead. - pub fn force_release_cursor(&mut self) -> Option<Index> { - self.context.force_release_cursor() - } - - /// Attempts to get the parent of the widget with the given ID - /// - /// A "valid" parent is simply one that does not have a render command of - /// [`RenderCommand::Empty`](crate::render_command::RenderCommand::Empty). - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// - pub fn get_valid_parent(&self, id: Index) -> Option<Index> { - self.context.widget_manager.get_valid_parent(id) - } - - /// Attempts to get the children of the widget with the given ID - /// - /// A "valid" child is simply one that does not have a render command of - /// [`RenderCommand::Empty`](crate::render_command::RenderCommand::Empty). - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// - pub fn get_valid_children(&self, id: Index) -> Vec<Index> { - self.context.widget_manager.get_valid_node_children(id) - } - - /// Attempts to get the layout rect for the widget with the given ID - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// - pub fn get_layout(&self, widget_id: &Index) -> Option<&crate::layout_cache::Rect> { - self.context.widget_manager.get_layout(widget_id) - } - - /// Get the render node for the widget with the given ID - /// - /// This is useful if you need access to the resolved styles, z-index, etc. of a widget. - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// - pub fn get_node(&self, id: &Index) -> Option<crate::node::Node> { - self.context.widget_manager.get_node(id) - } - - /// Attempts to get the name of the widget with the given ID - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// - pub fn get_name(&self, id: &Index) -> Option<String> { - self.context.widget_manager.get_name(id) - } - - /// Adds a widget to the context reference tree that will be committed to the main tree when `commit` is called. - /// This also adds the widget to the `KayakContext` and renders the new widget. - /// - /// # Arguments - /// - /// * `widget`: The widget to add - /// * `widget_index`: The widget's zero-based index amongst its siblings - /// - pub fn add_widget<W: crate::Widget>(&mut self, widget: W, widget_index: usize) { - let (_, child_id) = - self.context - .widget_manager - .create_widget(widget_index, widget, self.current_id); - self.tree.as_ref().unwrap().add(child_id, self.current_id); - - let mut child_widget = self.context.widget_manager.take(child_id); - { - let mut context = KayakContextRef::new(&mut self.context, Some(child_id)); - // TODO: Use context ref here instead - child_widget.render(&mut context); - } - self.context.widget_manager.repossess(child_widget); - } - - /// Consumes the `KayakContextRef`. Internally this commits the newly built tree to the main widget tree. - pub fn commit(&mut self) { - // Consume the widget tree taking the inner value - let tree = self.tree.take().unwrap().take(); - - // Evaluate changes to the tree. - let changes = self - .context - .widget_manager - .tree - .diff_children(&tree, self.current_id.unwrap_or_default()); - self.context - .widget_manager - .tree - .merge(&tree, self.current_id.unwrap_or_default(), changes); - } - - /// Marks the current widget as dirty (needing to be re-rendered) - /// - /// You should generally not need this. The point is to only re-render widgets when their state - /// changes. This method bypasses that and forcibly re-renders the widget which can be wasteful if - /// used improperly. - /// - /// Currently, this method is used internally for text rendering, which needs to re-render when - /// it's parent layout is calculated. - pub fn mark_dirty(&mut self) { - if let Ok(mut dirty_nodes) = self.context.widget_manager.dirty_nodes.lock() { - dirty_nodes.insert(self.current_id.unwrap_or_default()); - } - } -} - -#[test] -fn test_context_ref() { - use crate::binding::{Bound, MutableBound}; - use crate::context_ref::KayakContextRef; - - let mut kayak_context = KayakContext::new(); - let mut kayak_context_ref = KayakContextRef::new(&mut kayak_context, None); - - let state = kayak_context_ref.create_state(0f32).unwrap(); - - state.set(1.0); - - let state_value = state.get(); - assert!(state_value == 1.0); -} diff --git a/kayak_core/src/cursor_icon.rs b/kayak_core/src/cursor_icon.rs deleted file mode 100644 index 7becbec..0000000 --- a/kayak_core/src/cursor_icon.rs +++ /dev/null @@ -1,44 +0,0 @@ -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum CursorIcon { - Default, - Crosshair, - Hand, - Arrow, - Move, - Text, - Wait, - Help, - Progress, - NotAllowed, - ContextMenu, - Cell, - VerticalText, - Alias, - Copy, - NoDrop, - Grab, - Grabbing, - AllScroll, - ZoomIn, - ZoomOut, - EResize, - NResize, - NeResize, - NwResize, - SResize, - SeResize, - SwResize, - WResize, - EwResize, - NsResize, - NeswResize, - NwseResize, - ColResize, - RowResize, -} - -impl Default for CursorIcon { - fn default() -> Self { - CursorIcon::Default - } -} diff --git a/kayak_core/src/flo_binding/CHANGELOG b/kayak_core/src/flo_binding/CHANGELOG deleted file mode 100644 index de09838..0000000 --- a/kayak_core/src/flo_binding/CHANGELOG +++ /dev/null @@ -1,6 +0,0 @@ -A list of changes can be found here: -https://github.com/Logicalshift/flo_binding/compare/master...StarArawn:master - -Notable changes: -- Use send + sync for all types that just required Send before. -- Bindings are given ID's(UUID's) to track them. diff --git a/kayak_core/src/flo_binding/LICENSE b/kayak_core/src/flo_binding/LICENSE deleted file mode 100644 index 6b3388e..0000000 --- a/kayak_core/src/flo_binding/LICENSE +++ /dev/null @@ -1,203 +0,0 @@ -This license is for the flo_binding crate: - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/kayak_core/src/flo_binding/bind_stream.rs b/kayak_core/src/flo_binding/bind_stream.rs deleted file mode 100644 index 2f9e39a..0000000 --- a/kayak_core/src/flo_binding/bind_stream.rs +++ /dev/null @@ -1,228 +0,0 @@ -use super::binding_context::*; -use super::releasable::*; -use super::traits::*; - -use ::desync::*; -use futures::prelude::*; - -use std::sync::*; - -#[allow(dead_code)] -/// -/// Uses a stream to update a binding -/// -pub fn bind_stream<S, Value, UpdateFn>( - stream: S, - initial_value: Value, - update: UpdateFn, -) -> StreamBinding<Value> -where - S: 'static + Send + Stream + Unpin, - Value: 'static + Send + Clone + PartialEq, - UpdateFn: 'static + Send + FnMut(Value, S::Item) -> Value, - S::Item: Send, -{ - // Create the content of the binding - let value = Arc::new(Mutex::new(initial_value)); - let core = StreamBindingCore { - value: Arc::clone(&value), - notifications: vec![], - }; - - let core = Arc::new(Desync::new(core)); - let mut update = update; - - // Send in the stream - pipe_in(Arc::clone(&core), stream, move |core, next_item| { - // Only lock the value while updating it - let need_to_notify = { - // Update the value - let mut value = core.value.lock().unwrap(); - let new_value = update((*value).clone(), next_item); - - if new_value != *value { - // Update the value in the core - *value = new_value; - - // Notify anything that's listening - true - } else { - false - } - }; - - // If the update changed the value, then call the notifications (with the lock released, in case any try to read the value) - if need_to_notify { - core.notifications.retain(|notify| notify.is_in_use()); - core.notifications.iter().for_each(|notify| { - notify.mark_as_changed(); - }); - } - - Box::pin(future::ready(())) - }); - - StreamBinding { - core: core, - value: value, - } -} - -/// -/// Binding that represents the result of binding a stream to a value -/// -#[derive(Clone)] -pub struct StreamBinding<Value: Send> { - /// The core of the binding (where updates are streamed and notifications sent) - core: Arc<Desync<StreamBindingCore<Value>>>, - - /// The current value of the binding - value: Arc<Mutex<Value>>, -} - -/// -/// The data stored with a stream binding -/// -struct StreamBindingCore<Value: Send> { - /// The current value of this binidng - value: Arc<Mutex<Value>>, - - /// The items that should be notified when this binding changes - notifications: Vec<ReleasableNotifiable>, -} - -impl<Value: 'static + Send + Clone> Bound<Value> for StreamBinding<Value> { - /// - /// Retrieves the value stored by this binding - /// - fn get(&self) -> Value { - BindingContext::add_dependency(self.clone()); - - let value = self.value.lock().unwrap(); - (*value).clone() - } -} - -impl<Value: 'static + Send> Changeable for StreamBinding<Value> { - /// - /// Supplies a function to be notified when this item is changed - /// - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - // Create the notification object - let releasable = ReleasableNotifiable::new(what); - let notifiable = releasable.clone_as_owned(); - - // Send to the core - self.core.desync(move |core| { - core.notifications.push(notifiable); - }); - - // Return the releasable object - Box::new(releasable) - } -} - -#[cfg(test)] -mod test { - use super::super::notify_fn::*; - use super::*; - - use futures::channel::mpsc; - use futures::executor; - use futures::stream; - - use std::thread; - use std::time::Duration; - - #[test] - pub fn stream_in_all_values() { - // Stream with the values '1,2,3' - let stream = vec![1, 2, 3]; - let stream = stream::iter(stream.into_iter()); - - // Send the stream to a new binding - let binding = bind_stream(stream, 0, |_old_value, new_value| new_value); - - thread::sleep(Duration::from_millis(10)); - - // Binding should have the value of the last value in the stream - assert!(binding.get() == 3); - } - - #[test] - pub fn stream_processes_updates() { - // Stream with the values '1,2,3' - let stream = vec![1, 2, 3]; - let stream = stream::iter(stream.into_iter()); - - // Send the stream to a new binding (with some processing) - let binding = bind_stream(stream, 0, |_old_value, new_value| new_value + 42); - - thread::sleep(Duration::from_millis(10)); - - // Binding should have the value of the last value in the stream - assert!(binding.get() == 45); - } - - #[test] - pub fn notifies_on_change() { - // Create somewhere to send our notifications - let (mut sender, receiver) = mpsc::channel(0); - - // Send the receiver stream to a new binding - let binding = bind_stream(receiver, 0, |_old_value, new_value| new_value); - - // Create the notification - let notified = Arc::new(Mutex::new(false)); - let also_notified = Arc::clone(¬ified); - - binding - .when_changed(notify(move || *also_notified.lock().unwrap() = true)) - .keep_alive(); - - // Should be initially un-notified - thread::sleep(Duration::from_millis(5)); - assert!(*notified.lock().unwrap() == false); - - executor::block_on(async { - // Send a value to the sender - sender.send(42).await.unwrap(); - - // Should get notified - thread::sleep(Duration::from_millis(5)); - assert!(*notified.lock().unwrap() == true); - assert!(binding.get() == 42); - }) - } - - #[test] - pub fn no_notification_on_no_change() { - // Create somewhere to send our notifications - let (mut sender, receiver) = mpsc::channel(0); - - // Send the receiver stream to a new binding - let binding = bind_stream(receiver, 0, |_old_value, new_value| new_value); - - // Create the notification - let notified = Arc::new(Mutex::new(false)); - let also_notified = Arc::clone(¬ified); - - binding - .when_changed(notify(move || *also_notified.lock().unwrap() = true)) - .keep_alive(); - - // Should be initially un-notified - thread::sleep(Duration::from_millis(5)); - assert!(*notified.lock().unwrap() == false); - - executor::block_on(async { - // Send a value to the sender. This leaves the final value the same, so no notification should be generated. - sender.send(0).await.unwrap(); - - // Should not get notified - thread::sleep(Duration::from_millis(5)); - assert!(*notified.lock().unwrap() == false); - assert!(binding.get() == 0); - }); - } -} diff --git a/kayak_core/src/flo_binding/binding.rs b/kayak_core/src/flo_binding/binding.rs deleted file mode 100644 index b1ec3c3..0000000 --- a/kayak_core/src/flo_binding/binding.rs +++ /dev/null @@ -1,239 +0,0 @@ -use super::binding_context::*; -use super::releasable::*; -use super::traits::*; - -use std::sync::*; -pub use uuid::Uuid; - -/// -/// An internal representation of a bound value -/// -struct BoundValue<Value> { - /// The current value of this binding - value: Value, - - /// What to call when the value changes - when_changed: Vec<ReleasableNotifiable>, -} - -impl<Value: Clone + PartialEq> BoundValue<Value> { - /// - /// Creates a new binding with the specified value - /// - pub fn new(val: Value) -> BoundValue<Value> { - BoundValue { - value: val, - when_changed: vec![], - } - } - - /// - /// Updates the value in this structure without calling the notifications, returns whether or not anything actually changed - /// - pub fn set_without_notifying(&mut self, new_value: Value) -> bool { - let changed = self.value != new_value; - - self.value = new_value; - - changed - } - - /// - /// Retrieves a copy of the list of notifiable items for this value - /// - pub fn get_notifiable_items(&self) -> Vec<ReleasableNotifiable> { - self.when_changed - .iter() - .map(|item| item.clone_for_inspection()) - .collect() - } - - /// - /// If there are any notifiables in this object that aren't in use, remove them - /// - pub fn filter_unused_notifications(&mut self) { - self.when_changed - .retain(|releasable| releasable.is_in_use()); - } - - /// - /// Retrieves the value of this item - /// - fn get(&self) -> Value { - self.value.clone() - } - - /// - /// Retrieves a mutable reference to the value of this item - /// - fn get_mut(&mut self) -> &mut Value { - &mut self.value - } - - /// - /// Adds something that will be notified when this item changes - /// - fn when_changed(&mut self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - let releasable = ReleasableNotifiable::new(what); - self.when_changed.push(releasable.clone_as_owned()); - - self.filter_unused_notifications(); - - Box::new(releasable) - } -} - -impl<Value: Default + Clone + PartialEq> Default for BoundValue<Value> { - fn default() -> Self { - BoundValue::new(Value::default()) - } -} - -impl<Value: std::fmt::Debug> std::fmt::Debug for BoundValue<Value> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) - } -} - -impl<Value: PartialEq> PartialEq for BoundValue<Value> { - fn eq(&self, other: &Self) -> bool { - self.value.eq(&other.value) - } -} -/// -/// Represents a thread-safe, sharable binding -/// -#[derive(Clone)] -pub struct Binding<Value> { - pub id: Uuid, - /// The value stored in this binding - value: Arc<Mutex<BoundValue<Value>>>, -} - -impl<Value: Default + Clone + PartialEq> Default for Binding<Value> { - fn default() -> Self { - Binding::new(Value::default()) - } -} - -impl<Value: std::fmt::Debug> std::fmt::Debug for Binding<Value> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) - } -} - -impl<Value: PartialEq> PartialEq for Binding<Value> { - fn eq(&self, other: &Self) -> bool { - self.value.lock().unwrap().eq(&other.value.lock().unwrap()) - } -} - -impl<Value: Clone + PartialEq> Binding<Value> { - pub fn new(value: Value) -> Binding<Value> { - Binding { - id: Uuid::new_v4(), - value: Arc::new(Mutex::new(BoundValue::new(value))), - } - } -} - -impl<Value: 'static + Clone + PartialEq + Send + Sync> Changeable for Binding<Value> { - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - self.value.lock().unwrap().when_changed(what) - } -} - -impl<Value: 'static + Clone + PartialEq + Send + Sync> Bound<Value> for Binding<Value> { - fn get(&self) -> Value { - BindingContext::add_dependency(self.clone()); - - self.value.lock().unwrap().get() - } -} - -impl<Value: 'static + Clone + PartialEq + Send + Sync> MutableBound<Value> for Binding<Value> { - fn set(&self, new_value: Value) { - // Update the value with the lock held - let notifications = { - let mut cell = self.value.lock().unwrap(); - let changed = cell.set_without_notifying(new_value); - - if changed { - cell.get_notifiable_items() - } else { - vec![] - } - }; - - // Call the notifications outside of the lock - let mut needs_filtering = false; - - for to_notify in notifications { - needs_filtering = !to_notify.mark_as_changed() || needs_filtering; - } - - if needs_filtering { - let mut cell = self.value.lock().unwrap(); - cell.filter_unused_notifications(); - } - } -} - -impl<Value: 'static + Clone + PartialEq + Send + Sync> WithBound<Value> for Binding<Value> { - fn with_ref<F, T>(&self, f: F) -> T - where - F: FnOnce(&Value) -> T, - { - f(&self.value.lock().unwrap().value) - } - fn with_mut<F>(&self, f: F) - where - F: FnOnce(&mut Value) -> bool, - { - let notifications = { - let mut v = self.value.lock().unwrap(); - let changed = f(v.get_mut()); - - if changed { - v.get_notifiable_items() - } else { - vec![] - } - }; - - // Call the notifications outside of the lock - let mut needs_filtering = false; - - for to_notify in notifications { - needs_filtering = !to_notify.mark_as_changed() || needs_filtering; - } - - if needs_filtering { - let mut cell = self.value.lock().unwrap(); - cell.filter_unused_notifications(); - } - } -} - -impl<Value: 'static + Clone + PartialEq + Send + Sync> From<Value> for Binding<Value> { - #[inline] - fn from(val: Value) -> Binding<Value> { - Binding::new(val) - } -} - -impl<'a, Value: 'static + Clone + PartialEq + Send + Sync> From<&'a Binding<Value>> - for Binding<Value> -{ - #[inline] - fn from(val: &'a Binding<Value>) -> Binding<Value> { - Binding::clone(val) - } -} - -impl<'a, Value: 'static + Clone + PartialEq + Send + Sync> From<&'a Value> for Binding<Value> { - #[inline] - fn from(val: &'a Value) -> Binding<Value> { - Binding::new(val.clone()) - } -} diff --git a/kayak_core/src/flo_binding/binding_context.rs b/kayak_core/src/flo_binding/binding_context.rs deleted file mode 100644 index ea4ee4f..0000000 --- a/kayak_core/src/flo_binding/binding_context.rs +++ /dev/null @@ -1,187 +0,0 @@ -use super::notify_fn::*; -use super::traits::*; - -use std::cell::*; -use std::rc::*; -use std::sync::*; - -thread_local! { - static CURRENT_CONTEXT: RefCell<Option<BindingContext>> = RefCell::new(None); -} - -/// -/// Represents the dependencies of a binding context -/// -#[derive(Clone)] -pub struct BindingDependencies { - /// Set to true if the binding dependencies have been changed since they were registered in the dependencies - recently_changed: Arc<Mutex<bool>>, - - /// The when_changed monitors for the recently_changed flag - recent_change_monitors: Rc<RefCell<Vec<Box<dyn Releasable>>>>, - - /// The list of changables that are dependent on this context - dependencies: Rc<RefCell<Vec<Box<dyn Changeable>>>>, -} - -impl BindingDependencies { - /// - /// Creates a new binding dependencies object - /// - pub fn new() -> BindingDependencies { - BindingDependencies { - recently_changed: Arc::new(Mutex::new(false)), - recent_change_monitors: Rc::new(RefCell::new(vec![])), - dependencies: Rc::new(RefCell::new(vec![])), - } - } - - /// - /// Adds a new dependency to this object - /// - pub fn add_dependency<TChangeable: Changeable + 'static>(&mut self, dependency: TChangeable) { - // Set the recently changed flag so that we can tell if the dependencies are already out of date before when_changed is called - let recently_changed = Arc::clone(&self.recently_changed); - let mut recent_change_monitors = self.recent_change_monitors.borrow_mut(); - recent_change_monitors.push(dependency.when_changed(notify(move || { - *recently_changed.lock().unwrap() = true; - }))); - - // Add this dependency to the list - self.dependencies.borrow_mut().push(Box::new(dependency)) - } - - /// - /// If the dependencies have not changed since they were registered, registers for changes - /// and returns a `Releasable`. If the dependencies are already different, returns `None`. - /// - pub fn when_changed_if_unchanged( - &self, - what: Arc<dyn Notifiable>, - ) -> Option<Box<dyn Releasable>> { - let mut to_release = vec![]; - - // Register with all of the dependencies - for dep in self.dependencies.borrow_mut().iter_mut() { - to_release.push(dep.when_changed(Arc::clone(&what))); - } - - if *self.recently_changed.lock().unwrap() { - // If a value changed while we were building these dependencies, then immediately generate the notification - to_release - .into_iter() - .for_each(|mut releasable| releasable.done()); - - // Nothing to release - None - } else { - // Otherwise, return the set of releasable values - Some(Box::new(to_release)) - } - } -} - -impl Changeable for BindingDependencies { - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - let when_changed_or_not = self.when_changed_if_unchanged(Arc::clone(&what)); - - match when_changed_or_not { - Some(releasable) => releasable, - None => { - what.mark_as_changed(); - Box::new(vec![]) - } - } - } -} - -/// -/// Represents a binding context. Binding contexts are -/// per-thread structures, used to track -/// -#[derive(Clone)] -pub struct BindingContext { - /// The dependencies for this context - dependencies: BindingDependencies, - - /// None, or the binding context that this context was created within - _nested: Option<Box<BindingContext>>, -} - -impl BindingContext { - /// - /// Gets the active binding context - /// - pub fn current() -> Option<BindingContext> { - CURRENT_CONTEXT.with(|current_context| current_context.borrow().as_ref().cloned()) - } - - /// - /// Panics if we're trying to create a binding, with a particular message - /// - pub fn panic_if_in_binding_context(msg: &str) { - if CURRENT_CONTEXT.with(|context| context.borrow().is_some()) { - panic!("Not possible when binding: {}", msg); - } - } - - /// - /// Executes a function in a new binding context - /// - pub fn bind<TResult, TFn>(to_do: TFn) -> (TResult, BindingDependencies) - where - TFn: FnOnce() -> TResult, - { - // Remember the previous context - let previous_context = Self::current(); - - // Create a new context - let dependencies = BindingDependencies::new(); - let new_context = BindingContext { - dependencies: dependencies.clone(), - _nested: previous_context.clone().map(Box::new), - }; - - // Make the current context the same as the new context - CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = Some(new_context)); - - // Perform the requested action with this context - let result = to_do(); - - // Reset to the previous context - CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = previous_context); - - (result, dependencies) - } - - #[allow(dead_code)] - /// - /// Performs an action outside of the binding context (dependencies - /// will not be tracked for anything the supplied function does) - /// - pub fn out_of_context<TResult, TFn>(to_do: TFn) -> TResult - where - TFn: FnOnce() -> TResult, - { - // Remember the previous context - let previous_context = Self::current(); - - // Unset the context - CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = None); - - // Perform the operations without a binding context - let result = to_do(); - - // Reset to the previous context - CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = previous_context); - - result - } - - /// - /// Adds a dependency to the current context (if one is found) - /// - pub fn add_dependency<TChangeable: Changeable + 'static>(dependency: TChangeable) { - Self::current().map(|mut ctx| ctx.dependencies.add_dependency(dependency)); - } -} diff --git a/kayak_core/src/flo_binding/bindref.rs b/kayak_core/src/flo_binding/bindref.rs deleted file mode 100644 index ec4fa57..0000000 --- a/kayak_core/src/flo_binding/bindref.rs +++ /dev/null @@ -1,183 +0,0 @@ -#[cfg(feature = "stream")] -use super::bind_stream::*; -use super::binding::*; -use super::computed::*; -use super::traits::*; - -use std::sync::*; - -/// -/// A `BindRef` references another binding without needing to know precisely -/// what kind of binding it is. It is read-only, so mostly useful for passing -/// a binding around, particularly for computed bindings. Create one with -/// `BindRef::from(binding)`. -/// -/// Cloning a `BindRef` will create another reference to the same binding. -/// -pub struct BindRef<Target> { - reference: Arc<dyn Bound<Target>>, -} - -impl<Value> Bound<Value> for BindRef<Value> { - #[inline] - fn get(&self) -> Value { - self.reference.get() - } -} - -impl<Value> Changeable for BindRef<Value> { - #[inline] - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - self.reference.when_changed(what) - } -} - -impl<Value> Clone for BindRef<Value> { - fn clone(&self) -> Self { - BindRef { - reference: Arc::clone(&self.reference), - } - } -} - -impl<Value> BindRef<Value> { - /// - /// Creates a new BindRef from a reference to an existing binding - /// - #[inline] - #[allow(dead_code)] - pub fn new<Binding: 'static + Clone + Bound<Value>>(binding: &Binding) -> BindRef<Value> { - BindRef { - reference: Arc::new(binding.clone()), - } - } - - /// - /// Creates a new BindRef from an existing binding - /// - #[inline] - #[allow(dead_code)] - pub fn from_arc<Binding: 'static + Bound<Value>>(binding_ref: Arc<Binding>) -> BindRef<Value> { - BindRef { - reference: binding_ref, - } - } -} - -impl<'a, Value> From<&'a BindRef<Value>> for BindRef<Value> { - #[inline] - fn from(val: &'a BindRef<Value>) -> Self { - BindRef::clone(val) - } -} - -impl<Value: 'static + Clone + Send + Sync + PartialEq> From<Binding<Value>> for BindRef<Value> { - #[inline] - fn from(val: Binding<Value>) -> Self { - BindRef { - reference: Arc::new(val), - } - } -} - -impl<'a, Value: 'static + Clone + PartialEq + Sync + Send> From<&'a Binding<Value>> - for BindRef<Value> -{ - #[inline] - fn from(val: &'a Binding<Value>) -> Self { - BindRef { - reference: Arc::new(val.clone()), - } - } -} - -impl<Value: 'static + Clone + PartialEq + Sync + Send, TFn> From<ComputedBinding<Value, TFn>> - for BindRef<Value> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - #[inline] - fn from(val: ComputedBinding<Value, TFn>) -> Self { - BindRef { - reference: Arc::new(val), - } - } -} - -impl<'a, Value: 'static + Clone + PartialEq + Sync + Send, TFn> - From<&'a ComputedBinding<Value, TFn>> for BindRef<Value> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - #[inline] - fn from(val: &'a ComputedBinding<Value, TFn>) -> Self { - BindRef { - reference: Arc::new(val.clone()), - } - } -} - -impl<'a, Value: 'static + Clone + PartialEq + Send + Sync + Into<Binding<Value>>> From<&'a Value> - for BindRef<Value> -{ - #[inline] - fn from(val: &'a Value) -> BindRef<Value> { - let binding: Binding<Value> = val.into(); - BindRef::from(binding) - } -} - -#[cfg(feature = "stream")] -impl<Value: 'static + Clone + Send + Sync + PartialEq> From<StreamBinding<Value>> - for BindRef<Value> -{ - #[inline] - fn from(val: StreamBinding<Value>) -> Self { - BindRef { - reference: Arc::new(val), - } - } -} - -#[cfg(test)] -mod test { - use super::super::*; - - #[test] - fn bindref_matches_core_value() { - let bind = bind(1); - let bind_ref = BindRef::from(bind.clone()); - - assert!(bind_ref.get() == 1); - - bind.set(2); - - assert!(bind_ref.get() == 2); - } - - #[test] - fn bind_ref_from_value() { - let bind_ref = BindRef::from(&1); - - assert!(bind_ref.get() == 1); - } - - #[test] - fn bind_ref_from_computed() { - let bind_ref = BindRef::from(computed(|| 1)); - - assert!(bind_ref.get() == 1); - } - - #[test] - fn bindref_matches_core_value_when_created_from_ref() { - let bind = bind(1); - let bind_ref = BindRef::new(&bind); - - assert!(bind_ref.get() == 1); - - bind.set(2); - - assert!(bind_ref.get() == 2); - } -} diff --git a/kayak_core/src/flo_binding/computed.rs b/kayak_core/src/flo_binding/computed.rs deleted file mode 100644 index 16da92a..0000000 --- a/kayak_core/src/flo_binding/computed.rs +++ /dev/null @@ -1,347 +0,0 @@ -use super::binding_context::*; -use super::releasable::*; -use super::traits::*; - -use std::mem; -use std::sync::*; - -/// -/// Represents a computed value -/// -#[derive(Clone)] -enum ComputedValue<Value: 'static + Clone> { - Unknown, - Cached(Value), -} - -use self::ComputedValue::*; - -/// -/// Core representation ofa computed binding -/// -struct ComputedBindingCore<Value: 'static + Clone, TFn> -where - TFn: 'static + Fn() -> Value, -{ - /// Function to call to recalculate this item - calculate_value: TFn, - - /// Most recent cached value - latest_value: ComputedValue<Value>, - - /// If there's a notification attached to this item, this can be used to release it - existing_notification: Option<Box<dyn Releasable>>, - - /// What to call when the value changes - when_changed: Vec<ReleasableNotifiable>, -} - -impl<Value: 'static + Clone, TFn> ComputedBindingCore<Value, TFn> -where - TFn: 'static + Fn() -> Value, -{ - /// - /// Creates a new computed binding core item - /// - pub fn new(calculate_value: TFn) -> ComputedBindingCore<Value, TFn> { - ComputedBindingCore { - calculate_value: calculate_value, - latest_value: Unknown, - existing_notification: None, - when_changed: vec![], - } - } - - /// - /// Marks the value as changed, returning true if the value was removed - /// - pub fn mark_changed(&mut self) -> bool { - match self.latest_value { - Unknown => false, - _ => { - self.latest_value = Unknown; - true - } - } - } - - /// - /// Retrieves a copy of the list of notifiable items for this value - /// - pub fn get_notifiable_items(&self) -> Vec<ReleasableNotifiable> { - self.when_changed - .iter() - .map(|item| item.clone_for_inspection()) - .collect() - } - - /// - /// If there are any notifiables in this object that aren't in use, remove them - /// - pub fn filter_unused_notifications(&mut self) { - self.when_changed - .retain(|releasable| releasable.is_in_use()); - } - - /// - /// Returns the current value (or 'Unknown' if it needs recalculating) - /// - pub fn get(&self) -> ComputedValue<Value> { - self.latest_value.clone() - } - - /// - /// Recalculates the latest value - /// - pub fn recalculate(&mut self) -> (Value, BindingDependencies) { - // Perform the binding in a context to get the value and the dependencies - let (result, dependencies) = BindingContext::bind(|| (self.calculate_value)()); - - // Update the latest value - self.latest_value = Cached(result.clone()); - - // Pass on the result - (result, dependencies) - } -} - -impl<Value: 'static + Clone, TFn> Drop for ComputedBindingCore<Value, TFn> -where - TFn: 'static + Fn() -> Value, -{ - fn drop(&mut self) { - // No point receiving any notifications once the core has gone - // (The notification can still fire if it has a weak reference) - if let Some(ref mut existing_notification) = self.existing_notification { - existing_notification.done() - } - } -} - -/// -/// Represents a binding to a value that is computed by a function -/// -pub struct ComputedBinding<Value: 'static + Clone, TFn> -where - TFn: 'static + Fn() -> Value, -{ - /// The core where the binding data is stored - core: Arc<Mutex<ComputedBindingCore<Value, TFn>>>, -} - -impl<Value: 'static + Clone + Send, TFn> ComputedBinding<Value, TFn> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - /// - /// Creates a new computable binding - /// - pub fn new(calculate_value: TFn) -> ComputedBinding<Value, TFn> { - // Computed bindings created in a binding context will likely not be - // retained, so things won't update as you expect. - // - // We could add some special logic to retain them here, or we could - // just panic. Panicking is probably better as really what should be - // done is to evaluate the content of the computed value directly. - // This can happen if we call a function that returns a binding and - // it creates one rather than returning an existing one. - BindingContext::panic_if_in_binding_context("Cannot create computed bindings in a computed value calculation function (you should evaluate the value directly rather than create bindings)"); - - // Create the binding - ComputedBinding { - core: Arc::new(Mutex::new(ComputedBindingCore::new(calculate_value))), - } - } - - /// - /// Creates a new computable binding within another binding - /// - /// Normally this is considered an error (if the binding is not held anywhere - /// outside of the context, it will never generate an update). `new` panics - /// if it's called from within a context for this reason. - /// - /// If the purpose of a computed binding is to return other bindings, this - /// limitation does not apply, so this call is available - /// - pub fn new_in_context(calculate_value: TFn) -> ComputedBinding<Value, TFn> { - // Create the binding - ComputedBinding { - core: Arc::new(Mutex::new(ComputedBindingCore::new(calculate_value))), - } - } - - /// - /// Marks this computed binding as having changed - /// - fn mark_changed(&self, force_notify: bool) { - // We do the notifications and releasing while the lock is not retained - let (notifiable, releasable) = { - // Get the core - let mut core = self.core.lock().unwrap(); - - // Mark it as changed - let actually_changed = core.mark_changed() || force_notify; - - core.filter_unused_notifications(); - - // Get the items that need changing (once we've notified our dependencies that we're changed, we don't need to notify them again until we get recalculated) - let notifiable = if actually_changed { - core.get_notifiable_items() - } else { - vec![] - }; - - // Extract the releasable so we can release it after the lock has gone - let mut releasable: Option<Box<dyn Releasable>> = None; - mem::swap(&mut releasable, &mut core.existing_notification); - - // These values are needed outside of the lock - (notifiable, releasable) - }; - - // Don't want any more notifications from this source - releasable.map(|mut releasable| releasable.done()); - - // Notify anything that needs to be notified that this has changed - for to_notify in notifiable { - to_notify.mark_as_changed(); - } - } - - /// - /// Mark this item as changed whenever 'to_monitor' is changed - /// Core should already be locked, returns true if the value is already changed and we should immediately notify - /// - fn monitor_changes( - &self, - core: &mut ComputedBindingCore<Value, TFn>, - to_monitor: &mut BindingDependencies, - ) -> bool { - // We only keep a weak reference to the core here - let to_notify = Arc::downgrade(&self.core); - - // Monitor for changes (see below for the implementation against to_notify's type) - let lifetime = to_monitor.when_changed_if_unchanged(Arc::new(to_notify)); - let already_changed = lifetime.is_none(); - - // Store the lifetime - let mut last_notification = lifetime; - mem::swap(&mut last_notification, &mut core.existing_notification); - - // Any lifetime that was in the core before this one should be finished - last_notification.map(|mut last_notification| last_notification.done()); - - // Return if the value is already changed - already_changed - } -} - -/// -/// The weak reference to a core is generated in `monitor_changes`: this specifies what happens when a -/// notification is generated for such a reference. -/// -impl<Value, TFn> Notifiable for Weak<Mutex<ComputedBindingCore<Value, TFn>>> -where - Value: 'static + Clone + Send, - TFn: 'static + Send + Sync + Fn() -> Value, -{ - fn mark_as_changed(&self) { - // If the reference is still active, reconstitute a computed binding in order to call the mark_changed method - if let Some(to_notify) = self.upgrade() { - let to_notify = ComputedBinding { core: to_notify }; - to_notify.mark_changed(false); - } else if cfg!(debug_assertions) { - // We can carry on here, but this suggests a memory leak - if the core has gone, then its owning object should have stopped this event from firing - panic!("The core of a computed is gone but its notifcations have been left behind"); - } - } -} - -impl<Value: 'static + Clone + Send, TFn> Clone for ComputedBinding<Value, TFn> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - fn clone(&self) -> Self { - ComputedBinding { - core: Arc::clone(&self.core), - } - } -} - -impl<Value: 'static + Clone, TFn> Changeable for ComputedBinding<Value, TFn> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - let releasable = ReleasableNotifiable::new(what); - - // Lock the core and push this as a thing to perform when this value changes - let mut core = self.core.lock().unwrap(); - core.when_changed.push(releasable.clone_as_owned()); - - core.filter_unused_notifications(); - - Box::new(releasable) - } -} - -impl<Value: 'static + Clone + Send, TFn> Bound<Value> for ComputedBinding<Value, TFn> -where - TFn: 'static + Send + Sync + Fn() -> Value, -{ - fn get(&self) -> Value { - // This is a dependency of the current binding context - BindingContext::add_dependency(self.clone()); - - // Set to true if the value changes while we're reading it - // (presumably because it's updating rapidly) - let mut notify_immediately = false; - let result; - - { - // Borrow the core - let mut core = self.core.lock().unwrap(); - - if let Cached(value) = core.get() { - // The value already exists in this item - result = value; - } else { - // TODO: really want to recalculate without locking the core - can do this by moving the function out and doing the recalculation here - // TODO: locking the core and calling a function can result in deadlocks due to user code structure in particular against other bindings - // TODO: when we do recalculate without locking, we need to make sure that no extra invalidations arrived between when we started the calculation and when we stored the result - // TODO: if multiple calculations do occur outside the lock, we need to return only the most recent result so when_changed is fired correctly - - // Stop responding to notifications - let mut old_notification = None; - mem::swap(&mut old_notification, &mut core.existing_notification); - - if let Some(mut last_notification) = old_notification { - last_notification.done(); - } - - // Need to re-calculate the core - let (value, mut dependencies) = core.recalculate(); - - // If any of the dependencies change, mark this item as changed too - notify_immediately = self.monitor_changes(&mut core, &mut dependencies); - - // If we're going to notify, unset the value we've cached - if notify_immediately { - core.latest_value = ComputedValue::Unknown; - } - - // TODO: also need to make sure that any hooks we have are removed if we're only referenced via a hook - - // Return the value - result = value; - } - } - - // If there was a change while we were calculating the value, generate a notification - if notify_immediately { - self.mark_changed(true); - } - - result - } -} diff --git a/kayak_core/src/flo_binding/follow.rs b/kayak_core/src/flo_binding/follow.rs deleted file mode 100644 index 7c82ce0..0000000 --- a/kayak_core/src/flo_binding/follow.rs +++ /dev/null @@ -1,249 +0,0 @@ -use super::notify_fn::*; -use super::traits::*; - -use futures::task; -use futures::task::Poll; -use futures::*; - -use ::desync::*; - -use std::marker::PhantomData; -use std::pin::Pin; -use std::sync::*; - -/// -/// The state of the binding for a follow stream -/// -#[derive(Copy, Clone)] -#[allow(dead_code)] -enum FollowState { - Unchanged, - Changed, -} - -/// -/// Core data structures for a follow stream -/// -struct FollowCore<TValue, Binding: Bound<TValue>> { - /// Changed if the binding value has changed, or Unchanged if it is not changed - state: FollowState, - - /// What to notify when this item is changed - notify: Option<task::Waker>, - - /// The binding that this is following - binding: Arc<Binding>, - - /// Value is stored in the binding - value: PhantomData<TValue>, -} - -/// -/// Stream that follows the values of a binding -/// -pub struct FollowStream<TValue: Send + Unpin, Binding: Bound<TValue>> { - /// The core of this future - core: Arc<Desync<FollowCore<TValue, Binding>>>, - - /// Lifetime of the watcher - _watcher: Box<dyn Releasable>, -} - -impl<TValue: 'static + Send + Unpin, Binding: 'static + Bound<TValue>> Stream - for FollowStream<TValue, Binding> -{ - type Item = TValue; - - fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Option<Self::Item>> { - // If the core is in a 'changed' state, return the binding so we can fetch it - // Want to fetch the binding value outside of the lock as it can potentially change during calculation - let binding = self.core.sync(|core| { - match core.state { - FollowState::Unchanged => { - // Wake this future when changed - core.notify = Some(cx.waker().clone()); - None - } - - FollowState::Changed => { - // Value has changed since we were last notified: return the changed value - core.state = FollowState::Unchanged; - Some(Arc::clone(&core.binding)) - } - } - }); - - if let Some(binding) = binding { - Poll::Ready(Some(binding.get())) - } else { - Poll::Pending - } - } -} - -/// -/// Creates a stream from a binding -/// -#[allow(dead_code)] -pub fn follow<TValue: 'static + Send + Unpin, Binding: 'static + Bound<TValue>>( - binding: Binding, -) -> FollowStream<TValue, Binding> { - // Generate the initial core - let core = FollowCore { - state: FollowState::Changed, - notify: None, - binding: Arc::new(binding), - value: PhantomData, - }; - - // Notify whenever the binding changes - let core = Arc::new(Desync::new(core)); - let weak_core = Arc::downgrade(&core); - let watcher = core.sync(move |core| { - core.binding.when_changed(notify(move || { - if let Some(core) = weak_core.upgrade() { - let task = core.sync(|core| { - core.state = FollowState::Changed; - core.notify.take() - }); - task.map(|task| task.wake()); - } - })) - }); - - // Create the stream - FollowStream { - core: core, - _watcher: watcher, - } -} - -#[cfg(test)] -mod test { - use super::super::*; - use super::*; - - use futures::executor; - use futures::task::{waker_ref, ArcWake, Context}; - - use std::thread; - use std::time::Duration; - - struct NotifyNothing; - impl ArcWake for NotifyNothing { - fn wake_by_ref(_arc_self: &Arc<Self>) { - // zzz - } - } - - #[test] - fn follow_stream_has_initial_value() { - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let mut stream = follow(bind_ref); - - executor::block_on(async { - assert!(stream.next().await == Some(1)); - }); - } - - #[test] - fn follow_stream_updates() { - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let mut stream = follow(bind_ref); - - executor::block_on(async { - assert!(stream.next().await == Some(1)); - binding.set(2); - assert!(stream.next().await == Some(2)); - }); - } - - #[test] - fn computed_updates_during_read() { - // Computed value that takes a while to calculate (so we can always 'lose' the race between reading the value and starting a new update) - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let computed = computed(move || { - let val = bind_ref.get(); - thread::sleep(Duration::from_millis(300)); - val - }); - let mut stream = follow(computed); - - // Read from the stream in the background - let reader = Desync::new(vec![]); - let read_values = reader.after( - async move { - let result = vec![stream.next().await, stream.next().await]; - result - }, - |val, read_val| { - *val = read_val; - }, - ); - - // Short delay so the reader starts - thread::sleep(Duration::from_millis(10)); - - // Update the binding - binding.set(2); - - // Wait for the values to be read from the stream - let values_read_from_stream = reader.sync(|val| val.clone()); - - // First read should return '1' - assert!(values_read_from_stream[0] == Some(1)); - - // Second read should return '2' - assert!(values_read_from_stream[1] == Some(2)); - - // Finish the read_values future - executor::block_on(read_values).unwrap(); - } - - #[test] - fn stream_is_unready_after_first_read() { - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let waker = Arc::new(NotifyNothing); - let waker = waker_ref(&waker); - let mut context = Context::from_waker(&waker); - let mut stream = follow(bind_ref); - - assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(1))); - assert!(stream.poll_next_unpin(&mut context) == Poll::Pending); - } - - #[test] - fn stream_is_immediately_ready_after_write() { - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let waker = Arc::new(NotifyNothing); - let waker = waker_ref(&waker); - let mut context = Context::from_waker(&waker); - let mut stream = follow(bind_ref); - - assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(1))); - binding.set(2); - assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(2))); - } - - #[test] - fn will_wake_when_binding_is_updated() { - let binding = bind(1); - let bind_ref = BindRef::from(binding.clone()); - let mut stream = follow(bind_ref); - - thread::spawn(move || { - thread::sleep(Duration::from_millis(100)); - binding.set(2); - }); - - executor::block_on(async { - assert!(stream.next().await == Some(1)); - assert!(stream.next().await == Some(2)); - }) - } -} diff --git a/kayak_core/src/flo_binding/mod.rs b/kayak_core/src/flo_binding/mod.rs deleted file mode 100644 index 20733f6..0000000 --- a/kayak_core/src/flo_binding/mod.rs +++ /dev/null @@ -1,736 +0,0 @@ -//! -//! # `flo_binding`, a data-driven binding library -//! -//! `flo_binding` is a library of types intended to help store state in interactive -//! applications. A binding is a mutable value that can notify observers when it -//! changes, or can supply its values as a stream. This is intended to make it easy -//! to build 'data-driven' applications, where updates to model state automatically -//! drive updates to the rest of the application. -//! -//! This method of propagating state is sometimes called 'reactive programming', -//! and has also been called 'parametric programming' in the past: it's most notable -//! for being the methodology behind spreadsheets and CAD applications before its -//! recent rediscovery in web applications. `flo_binding` prefers the term -//! 'data-driven' as it's a more plain description of how changes are pushed through -//! an application using the library. -//! -//! The advantage of using a binding library like this over a standard event-driven -//! architecture is that it's not necessary to send events or manually update state -//! when updating the application data model. Bugs are reduced because it's very -//! much harder for a dependent state update to go missing as this library can -//! automatically associate. -//! -//! There are three main types of binding provided by the `flo_binding` library: -//! -//! A normal binding, created by `bind(value)` can be updated to a new value by -//! calling `set()`. These are useful for representing state that is directly -//! manipulated by the user. -//! -//! A computed binding, created by `compute(move || { some_func() })`. When a -//! computed binding accesses the value of another binding by calling `get()`, it -//! will automatically monitor it for changes and indicate that it has changed -//! whenever that value changes. This makes it easy to create chains of dependent -//! bindings. -//! -//! A stream binding, created by `bind_stream(stream, initial_value, |old_value, new_value| { /* ... */ });`. -//! This makes it possible for a binding to update its values in response to a -//! stream of events, such as might be available from a UI framework. -//! -//! To make using computed bindings easier to generate, cloning a binding creates -//! a reference to the same piece of state. ie: `let a = bind(1); let b = a.clone();` -//! will give a and b a reference to the same value, so `a.set(2); b.get()` will -//! return 2. -//! -//! ## Event-driven architecture -//! -//! The `when_changed()` function can be called to register a function that is called -//! whenever a binding's value becomes invalidated. Once invalidated, a binding -//! remains invalidated until it's read with `get()`, which avoids computing values -//! for computed bindings that are never used. This can be used to integrate with -//! traditional UI frameworks. -//! -//! Calling `when_changed()` will return a lifetime object: the event will stop firing -//! when this object is dropped (or when `done()` is called on it). `keep_alive()` can -//! be used to keep the event firing for as long as the binding exists if necessary. -//! -//! The event-driven approach to application design has many disadvantages: the need to -//! manage event lifetimes is one, and another important one is the need to pass the -//! actual bindings around in order to attach events to them, so `flo_binding` provides -//! an alternative approach. -//! -//! ## Stream-driven architecture -//! -//! A superior alternative to the traditional OO-style event-driven architecture is -//! to take a stream-driven approach. Unlike events, streams manage their own lifetimes -//! and can be combined or split, and it's possible to pass around a stream without also -//! providing access to its source, which is a property that can be exploited to produce -//! a less interdependent application. FlowBetween, the application that flo_binding was -//! developed for is an example of a stream-driven application. -//! -//! Calling `follow(binding)` will generate a stream of updates from a binding. This -//! can be used to update part of a user interface directly. It can also be used as -//! an input for `bind_stream()` to create more complicated update rules than are -//! allowed by a simple computed binding. -//! -//! The companion library `desync` provides the `pipe()` and `pipe_in()` functions -//! which can be used to schedule events generated by a stream. -//! -//! ## Example -//! -//! ``` -//! # use flo_binding::*; -//! let mut number = bind(1); -//! let number_clone = number.clone(); -//! let plusone = computed(move || number_clone.get() + 1); -//! -//! let mut lifetime = plusone.when_changed(notify(|| println!("Changed!"))); -//! -//! println!("{}", plusone.get()); // 2 -//! # assert!(plusone.get() == 2); -//! number.set(2); // 'Changed!' -//! println!("{}", plusone.get()); // 3 -//! # assert!(plusone.get() == 3); -//! lifetime.done(); -//! -//! number.set(3); // Lifetime is done, so no notification -//! println!("{}", plusone.get()); // 4 -//! # assert!(plusone.get() == 4); -//! ``` -//! -#![warn(bare_trait_objects)] - -mod bind_stream; -mod binding; -pub mod binding_context; -mod bindref; -mod computed; -mod follow; -mod notify_fn; -mod releasable; -mod rope_binding; -mod traits; - -pub use self::bind_stream::*; -pub use self::binding::*; -pub use self::bindref::*; -pub use self::computed::*; -pub use self::follow::*; -pub use self::notify_fn::*; -pub use self::rope_binding::*; -pub use self::traits::*; - -/// -/// Creates a simple bound value with the specified initial value -/// -pub fn bind<Value: Clone + PartialEq>(val: Value) -> Binding<Value> { - Binding::new(val) -} - -/// -/// Creates a computed value that tracks bindings accessed during the function call and marks itself as changed when any of these dependencies also change -/// -pub fn computed<Value, TFn>(calculate_value: TFn) -> ComputedBinding<Value, TFn> -where - Value: Clone + Send, - TFn: 'static + Send + Sync + Fn() -> Value, -{ - ComputedBinding::new(calculate_value) -} - -#[cfg(test)] -mod test { - use super::binding_context::*; - use super::*; - - use std::sync::*; - use std::thread; - use std::time::Duration; - - #[test] - fn can_create_binding() { - let bound = bind(1); - assert!(bound.get() == 1); - } - - #[test] - fn can_update_binding() { - let bound = bind(1); - - bound.set(2); - assert!(bound.get() == 2); - } - - #[test] - fn notified_on_change() { - let bound = bind(1); - let changed = bind(false); - - let notify_changed = changed.clone(); - bound - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(changed.get() == false); - bound.set(2); - assert!(changed.get() == true); - } - - #[test] - fn not_notified_on_no_change() { - let bound = bind(1); - let changed = bind(false); - - let notify_changed = changed.clone(); - bound - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(changed.get() == false); - bound.set(1); - assert!(changed.get() == false); - } - - #[test] - fn notifies_after_each_change() { - let bound = bind(1); - let change_count = bind(0); - - let notify_count = change_count.clone(); - bound - .when_changed(notify(move || { - let count = notify_count.get(); - notify_count.set(count + 1) - })) - .keep_alive(); - - assert!(change_count.get() == 0); - bound.set(2); - assert!(change_count.get() == 1); - - bound.set(3); - assert!(change_count.get() == 2); - - bound.set(4); - assert!(change_count.get() == 3); - } - - #[test] - fn dispatches_multiple_notifications() { - let bound = bind(1); - let change_count = bind(0); - - let notify_count = change_count.clone(); - let notify_count2 = change_count.clone(); - bound - .when_changed(notify(move || { - let count = notify_count.get(); - notify_count.set(count + 1) - })) - .keep_alive(); - bound - .when_changed(notify(move || { - let count = notify_count2.get(); - notify_count2.set(count + 1) - })) - .keep_alive(); - - assert!(change_count.get() == 0); - bound.set(2); - assert!(change_count.get() == 2); - - bound.set(3); - assert!(change_count.get() == 4); - - bound.set(4); - assert!(change_count.get() == 6); - } - - #[test] - fn stops_notifying_after_release() { - let bound = bind(1); - let change_count = bind(0); - - let notify_count = change_count.clone(); - let mut lifetime = bound.when_changed(notify(move || { - let count = notify_count.get(); - notify_count.set(count + 1) - })); - - assert!(change_count.get() == 0); - bound.set(2); - assert!(change_count.get() == 1); - - lifetime.done(); - assert!(change_count.get() == 1); - bound.set(3); - assert!(change_count.get() == 1); - } - - #[test] - fn release_only_affects_one_notification() { - let bound = bind(1); - let change_count = bind(0); - - let notify_count = change_count.clone(); - let notify_count2 = change_count.clone(); - let mut lifetime = bound.when_changed(notify(move || { - let count = notify_count.get(); - notify_count.set(count + 1) - })); - bound - .when_changed(notify(move || { - let count = notify_count2.get(); - notify_count2.set(count + 1) - })) - .keep_alive(); - - assert!(change_count.get() == 0); - bound.set(2); - assert!(change_count.get() == 2); - - bound.set(3); - assert!(change_count.get() == 4); - - bound.set(4); - assert!(change_count.get() == 6); - - lifetime.done(); - - bound.set(5); - assert!(change_count.get() == 7); - - bound.set(6); - assert!(change_count.get() == 8); - - bound.set(7); - assert!(change_count.get() == 9); - } - - #[test] - fn binding_context_is_notified() { - let bound = bind(1); - - bound.set(2); - - let (value, context) = BindingContext::bind(|| bound.get()); - assert!(value == 2); - - let changed = bind(false); - let notify_changed = changed.clone(); - context - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(changed.get() == false); - bound.set(3); - assert!(changed.get() == true); - } - - #[test] - fn can_compute_value() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - assert!(computed.get() == 2); - } - - #[test] - fn can_recompute_value() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - assert!(computed.get() == 2); - - bound.set(2); - assert!(computed.get() == 3); - - bound.set(3); - assert!(computed.get() == 4); - } - - #[test] - fn can_recursively_compute_values() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed_val = computed(move || computed_from.get() + 1); - - let more_computed_from = computed_val.clone(); - let more_computed = computed(move || more_computed_from.get() + 1); - - assert!(computed_val.get() == 2); - assert!(more_computed.get() == 3); - - bound.set(2); - assert!(computed_val.get() == 3); - assert!(more_computed.get() == 4); - - bound.set(3); - assert!(computed_val.get() == 4); - assert!(more_computed.get() == 5); - } - - #[test] - fn can_recursively_compute_values_2() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed_val = computed(move || computed_from.get() + 1); - let more_computed = computed(move || computed_val.get() + 1); - - assert!(more_computed.get() == 3); - - bound.set(2); - assert!(more_computed.get() == 4); - - bound.set(3); - assert!(more_computed.get() == 5); - } - - #[test] - fn can_recursively_compute_values_3() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed_val = computed(move || computed_from.get() + 1); - let more_computed = computed(move || computed_val.get() + 1); - let even_more_computed = computed(move || more_computed.get() + 1); - - assert!(even_more_computed.get() == 4); - - bound.set(2); - assert!(even_more_computed.get() == 5); - - bound.set(3); - assert!(even_more_computed.get() == 6); - } - - #[test] - #[should_panic] - fn panics_if_computed_generated_during_binding() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed_val = computed(move || computed_from.get() + 1); - let even_more_computed = computed(move || { - let computed_val = computed_val.clone(); - - // This computed binding would be dropped after the first evaluation, which would result in the binding never updating. - // We should panic here. - let more_computed = computed(move || computed_val.get() + 1); - more_computed.get() + 1 - }); - - assert!(even_more_computed.get() == 4); - - bound.set(2); - assert!(even_more_computed.get() == 5); - - bound.set(3); - assert!(even_more_computed.get() == 6); - } - - #[test] - fn computed_only_recomputes_as_needed() { - let bound = bind(1); - - let counter = Arc::new(Mutex::new(0)); - let compute_counter = counter.clone(); - let computed_from = bound.clone(); - let computed = computed(move || { - let mut counter = compute_counter.lock().unwrap(); - *counter = *counter + 1; - - computed_from.get() + 1 - }); - - assert!(computed.get() == 2); - { - let counter = counter.lock().unwrap(); - assert!(counter.clone() == 1); - } - - assert!(computed.get() == 2); - { - let counter = counter.lock().unwrap(); - assert!(counter.clone() == 1); - } - - bound.set(2); - assert!(computed.get() == 3); - { - let counter = counter.lock().unwrap(); - assert!(counter.clone() == 2); - } - } - - #[test] - fn computed_caches_values() { - let update_count = Arc::new(Mutex::new(0)); - let bound = bind(1); - - let computed_update_count = Arc::clone(&update_count); - let computed_from = bound.clone(); - let computed = computed(move || { - let mut computed_update_count = computed_update_count.lock().unwrap(); - *computed_update_count += 1; - - computed_from.get() + 1 - }); - - assert!(computed.get() == 2); - assert!(*update_count.lock().unwrap() == 1); - - assert!(computed.get() == 2); - assert!(*update_count.lock().unwrap() == 1); - - bound.set(2); - assert!(computed.get() == 3); - assert!(*update_count.lock().unwrap() == 2); - - bound.set(3); - assert!(*update_count.lock().unwrap() == 2); - assert!(computed.get() == 4); - assert!(*update_count.lock().unwrap() == 3); - } - - #[test] - fn computed_notifies_of_changes() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - let changed = bind(false); - let notify_changed = changed.clone(); - computed - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(computed.get() == 2); - assert!(changed.get() == false); - - bound.set(2); - assert!(changed.get() == true); - assert!(computed.get() == 3); - - changed.set(false); - bound.set(3); - assert!(changed.get() == true); - assert!(computed.get() == 4); - } - - #[test] - fn computed_switches_dependencies() { - let switch = bind(false); - let val1 = bind(1); - let val2 = bind(2); - - let computed_switch = switch.clone(); - let computed_val1 = val1.clone(); - let computed_val2 = val2.clone(); - let computed = computed(move || { - // Use val1 when switch is false, and val2 when switch is true - if computed_switch.get() { - computed_val2.get() + 1 - } else { - computed_val1.get() + 1 - } - }); - - let changed = bind(false); - let notify_changed = changed.clone(); - computed - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - // Initial value of computed (first get 'arms' when_changed too) - assert!(computed.get() == 2); - assert!(changed.get() == false); - - // Setting val2 shouldn't cause computed to become 'changed' initially - val2.set(3); - assert!(changed.get() == false); - assert!(computed.get() == 2); - - // ... but setting val1 should - val1.set(2); - assert!(changed.get() == true); - assert!(computed.get() == 3); - - // Flicking the switch will use the val2 value we set earlier - changed.set(false); - switch.set(true); - assert!(changed.get() == true); - assert!(computed.get() == 4); - - // Updating val2 should now mark us as changed - changed.set(false); - val2.set(4); - assert!(changed.get() == true); - assert!(computed.get() == 5); - - // Updating val1 should not mark us as changed - changed.set(false); - val1.set(5); - assert!(changed.get() == false); - assert!(computed.get() == 5); - } - - #[test] - fn change_during_computation_recomputes() { - // Create a computed binding that delays for a bit while reading - let some_binding = bind(1); - let some_computed = { - let some_binding = some_binding.clone(); - computed(move || { - let result = some_binding.get() + 1; - thread::sleep(Duration::from_millis(250)); - result - }) - }; - - // Start a thread that reads a value - { - let some_computed = some_computed.clone(); - thread::spawn(move || { - assert!(some_computed.get() == 2); - }); - } - - // Let the thread start running (give it enough time to start computing and reach the sleep statement) - // TODO: thread::sleep might fail on systems that are slow enough or due to glitches (will fail spuriously if we update the binding before the calculation starts) - thread::sleep(Duration::from_millis(10)); - - // Update the value in the binding while the computed is running - some_binding.set(2); - - // Computed value should update - assert!(some_computed.get() == 3); - } - - #[test] - fn computed_propagates_changes() { - let bound = bind(1); - - let computed_from = bound.clone(); - let propagates_from = computed(move || computed_from.get() + 1); - let computed_propagated = propagates_from.clone(); - let computed = computed(move || computed_propagated.get() + 1); - - let changed = bind(false); - let notify_changed = changed.clone(); - computed - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(propagates_from.get() == 2); - assert!(computed.get() == 3); - assert!(changed.get() == false); - - bound.set(2); - assert!(propagates_from.get() == 3); - assert!(computed.get() == 4); - assert!(changed.get() == true); - - changed.set(false); - bound.set(3); - assert!(changed.get() == true); - assert!(propagates_from.get() == 4); - assert!(computed.get() == 5); - } - - #[test] - fn computed_stops_notifying_when_released() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - let changed = bind(false); - let notify_changed = changed.clone(); - let mut lifetime = computed.when_changed(notify(move || notify_changed.set(true))); - - assert!(computed.get() == 2); - assert!(changed.get() == false); - - bound.set(2); - assert!(changed.get() == true); - assert!(computed.get() == 3); - - changed.set(false); - lifetime.done(); - - bound.set(3); - assert!(changed.get() == false); - assert!(computed.get() == 4); - - bound.set(4); - assert!(changed.get() == false); - assert!(computed.get() == 5); - } - - #[test] - fn computed_doesnt_notify_more_than_once() { - let bound = bind(1); - - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - let changed = bind(false); - let notify_changed = changed.clone(); - computed - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(computed.get() == 2); - assert!(changed.get() == false); - - // Setting the value marks the computed as changed - bound.set(2); - assert!(changed.get() == true); - changed.set(false); - - // ... but when it's already changed we don't notify again - bound.set(3); - assert!(changed.get() == false); - - assert!(computed.get() == 4); - - // Once we've retrieved the value, we'll get notified of changes again - bound.set(4); - assert!(changed.get() == true); - } - - #[test] - fn computed_stops_notifying_once_out_of_scope() { - let bound = bind(1); - let changed = bind(false); - - { - let computed_from = bound.clone(); - let computed = computed(move || computed_from.get() + 1); - - let notify_changed = changed.clone(); - computed - .when_changed(notify(move || notify_changed.set(true))) - .keep_alive(); - - assert!(computed.get() == 2); - assert!(changed.get() == false); - - bound.set(2); - assert!(changed.get() == true); - assert!(computed.get() == 3); - }; - - // The computed value should have been disposed of so we should get no more notifications once we reach here - changed.set(false); - bound.set(3); - assert!(changed.get() == false); - } -} diff --git a/kayak_core/src/flo_binding/notify_fn.rs b/kayak_core/src/flo_binding/notify_fn.rs deleted file mode 100644 index d721e39..0000000 --- a/kayak_core/src/flo_binding/notify_fn.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::traits::*; - -use std::sync::*; - -struct NotifyFn<TFn> { - when_changed: Mutex<TFn>, -} - -impl<TFn> Notifiable for NotifyFn<TFn> -where - TFn: Send + FnMut() -> (), -{ - fn mark_as_changed(&self) { - let on_changed = &mut *self.when_changed.lock().unwrap(); - - on_changed() - } -} - -/// -/// Creates a notifiable reference from a function -/// -pub fn notify<TFn>(when_changed: TFn) -> Arc<dyn Notifiable> -where - TFn: 'static + Send + FnMut() -> (), -{ - Arc::new(NotifyFn { - when_changed: Mutex::new(when_changed), - }) -} diff --git a/kayak_core/src/flo_binding/releasable.rs b/kayak_core/src/flo_binding/releasable.rs deleted file mode 100644 index e41626f..0000000 --- a/kayak_core/src/flo_binding/releasable.rs +++ /dev/null @@ -1,135 +0,0 @@ -use super::traits::*; - -use std::sync::*; - -// TODO: issue with new 'drop' behaviour is what to do when we clone, as if keep_alive is -// false on the clone then dropping the clone will also drop this object. Sometimes we -// want this behaviour and sometimes we don't. - -/// -/// A notifiable that can be released (and then tidied up later) -/// -pub struct ReleasableNotifiable { - /// Set to true if this object should not release on drop. Note this is not shared, - /// so the first ReleasableNotifiable in a group to be dropped where keep_alive - /// is false will mark all the others as done. - keep_alive: bool, - - /// The notifiable object that should be released when it's done - target: Arc<Mutex<Option<Arc<dyn Notifiable>>>>, -} - -impl ReleasableNotifiable { - /// - /// Creates a new releasable notifiable object - /// - pub fn new(target: Arc<dyn Notifiable>) -> ReleasableNotifiable { - ReleasableNotifiable { - keep_alive: false, - target: Arc::new(Mutex::new(Some(target))), - } - } - - /// - /// Marks this as changed and returns whether or not the notification was called - /// - pub fn mark_as_changed(&self) -> bool { - // Get a reference to the target via the lock - let target = { - // Reset the optional item so that it's 'None' - let target = self.target.lock().unwrap(); - - // Send to the target - target.clone() - }; - - // Send to the target - if let Some(ref target) = target { - target.mark_as_changed(); - true - } else { - false - } - } - - /// - /// True if this item is still in use - /// - pub fn is_in_use(&self) -> bool { - self.target.lock().unwrap().is_some() - } - - /// - /// Creates a new 'owned' clone (which will expire this notifiable when dropped) - /// - pub fn clone_as_owned(&self) -> ReleasableNotifiable { - ReleasableNotifiable { - keep_alive: self.keep_alive, - target: Arc::clone(&self.target), - } - } - - /// - /// Creates a new 'inspection' clone (which can be dropped without ending - /// the lifetime of the releasable object) - /// - pub fn clone_for_inspection(&self) -> ReleasableNotifiable { - ReleasableNotifiable { - keep_alive: true, - target: Arc::clone(&self.target), - } - } -} - -impl Releasable for ReleasableNotifiable { - fn done(&mut self) { - // Reset the optional item so that it's 'None' - let mut target = self.target.lock().unwrap(); - - *target = None; - } - - fn keep_alive(&mut self) { - self.keep_alive = true; - } -} - -impl Notifiable for ReleasableNotifiable { - fn mark_as_changed(&self) { - // Get a reference to the target via the lock - let target = { - // Reset the optional item so that it's 'None' - let target = self.target.lock().unwrap(); - - // Send to the target - target.clone() - }; - - // Make sure we're calling out to mark_as_changed outside of the lock - if let Some(target) = target { - target.mark_as_changed(); - } - } -} - -impl Drop for ReleasableNotifiable { - fn drop(&mut self) { - if !self.keep_alive { - self.done(); - } - } -} - -impl Releasable for Vec<Box<dyn Releasable>> { - fn done(&mut self) { - for item in self.iter_mut() { - item.done(); - } - } - - fn keep_alive(&mut self) { - for item in self.iter_mut() { - item.keep_alive(); - } - } -} diff --git a/kayak_core/src/flo_binding/rope_binding/core.rs b/kayak_core/src/flo_binding/rope_binding/core.rs deleted file mode 100644 index c807027..0000000 --- a/kayak_core/src/flo_binding/rope_binding/core.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::flo_binding::releasable::*; -use crate::flo_binding::rope_binding::stream_state::*; - -use flo_rope::*; -use futures::task::*; - -/// -/// The core of a rope binding represents the data that's shared amongst all ropes -/// -pub(super) struct RopeBindingCore<Cell, Attribute> -where - Cell: Clone + PartialEq, - Attribute: Clone + PartialEq + Default, -{ - /// The number of items that are using hte core - pub(super) usage_count: usize, - - /// The rope that stores this binding - pub(super) rope: PullRope<AttributedRope<Cell, Attribute>, Box<dyn Fn() -> () + Send + Sync>>, - - /// The states of any streams reading from this rope - pub(super) stream_states: Vec<RopeStreamState<Cell, Attribute>>, - - #[allow(dead_code)] - /// The next ID to assign to a stream state - pub(super) next_stream_id: usize, - - // List of things to call when this binding changes - pub(super) when_changed: Vec<ReleasableNotifiable>, -} - -impl<Cell, Attribute> RopeBindingCore<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// If there are any notifiables in this object that aren't in use, remove them - /// - pub(super) fn filter_unused_notifications(&mut self) { - self.when_changed - .retain(|releasable| releasable.is_in_use()); - } - - /// - /// Callback: the rope has changes to pull - /// - pub(super) fn on_pull(&mut self) { - // Clear out any notifications that are not being used any more - self.filter_unused_notifications(); - - // Notify anything that's listening - for notifiable in &self.when_changed { - notifiable.mark_as_changed(); - } - - // Wake any streams that are waiting for changes to be pulled - for stream in self.stream_states.iter_mut() { - let waker = stream.waker.take(); - - if let Some(waker) = waker { - // Wake the stream so that it pulls the changes - waker.wake(); - } else { - // If the stream is trying to sleep, make sure it wakes up immediately - stream.needs_pull = true; - } - } - } - - /// - /// Pulls values from the rope and send to all attached streams - /// - pub(super) fn pull_rope(&mut self) { - // Stop the streams from waking up (no changes pending) - for stream in self.stream_states.iter_mut() { - stream.needs_pull = false; - } - - // Collect the actions - let actions = self.rope.pull_changes().collect::<Vec<_>>(); - - // Don't wake anything if there are no actions to perform - if actions.len() == 0 { - return; - } - - // Push to each stream - for stream in self.stream_states.iter_mut() { - stream.pending_changes.extend(actions.iter().cloned()); - } - - // Wake all of the streams - for stream in self.stream_states.iter_mut() { - let waker = stream.waker.take(); - waker.map(|waker| waker.wake()); - } - } - - /// - /// Wakes a particular stream when the rope changes - /// - pub(super) fn wake_stream(&mut self, stream_id: usize, waker: Waker) { - self.stream_states - .iter_mut() - .filter(|state| state.identifier == stream_id) - .nth(0) - .map(move |state| { - if !state.needs_pull { - // There are no pending values so we should wait for the rope to pull some extra data - - // Wake the stream when there's some more data to receive - state.waker = Some(waker); - } else { - // There are pending values so we should immediately re-awaken the stream - - // Disable the waker in case there's a stale one - state.waker = None; - - // Wake the stream so it reads the next value - waker.wake(); - } - }); - } -} diff --git a/kayak_core/src/flo_binding/rope_binding/mod.rs b/kayak_core/src/flo_binding/rope_binding/mod.rs deleted file mode 100644 index a88c0d3..0000000 --- a/kayak_core/src/flo_binding/rope_binding/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod core; -mod rope_binding; -mod rope_binding_mut; -mod stream; -mod stream_state; -#[cfg(test)] -mod tests; - -pub use self::rope_binding::*; -pub use self::rope_binding_mut::*; -pub use self::stream::*; diff --git a/kayak_core/src/flo_binding/rope_binding/rope_binding.rs b/kayak_core/src/flo_binding/rope_binding/rope_binding.rs deleted file mode 100644 index deca35b..0000000 --- a/kayak_core/src/flo_binding/rope_binding/rope_binding.rs +++ /dev/null @@ -1,255 +0,0 @@ -use crate::flo_binding::releasable::*; -use crate::flo_binding::rope_binding::core::*; -use crate::flo_binding::rope_binding::stream::*; -use crate::flo_binding::rope_binding::stream_state::*; -use crate::flo_binding::traits::*; - -use ::desync::*; -use flo_rope::*; -use futures::prelude::*; - -use std::collections::VecDeque; -use std::ops::Range; -use std::sync::*; - -/// -/// A rope binding binds a vector of cells and attributes -/// -/// It's also possible to use a normal `Binding<Vec<_>>` for this purpose. A rope binding has a -/// couple of advantages though: it can handle very large collections of items and it can notify -/// only the relevant changes instead of always notifying the entire structure. -/// -/// Rope bindings are ideal for representing text areas in user interfaces, but can be used for -/// any collection data structure. -/// -pub struct RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Unpin + Clone + PartialEq + Default, -{ - /// The core of this binding - core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, -} - -impl<Cell, Attribute> RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Creates a new rope binding from a stream of changes - /// - #[allow(dead_code)] - pub fn from_stream<S: 'static + Stream<Item = RopeAction<Cell, Attribute>> + Unpin + Send>( - stream: S, - ) -> RopeBinding<Cell, Attribute> { - // Create the core - let core = RopeBindingCore { - usage_count: 1, - rope: PullRope::from(AttributedRope::new(), Box::new(|| {})), - stream_states: vec![], - next_stream_id: 0, - when_changed: vec![], - }; - - let core = Arc::new(Desync::new(core)); - - // Recreate the rope in the core with a version that responds to pull events - let weak_core = Arc::downgrade(&core); - core.sync(move |core| { - core.rope = PullRope::from( - AttributedRope::new(), - Box::new(move || { - // Pass the event through to the core - let core = weak_core.upgrade(); - if let Some(core) = core { - core.desync(|core| core.on_pull()); - } - }), - ); - }); - - // Push changes through to the core rope from the stream - pipe_in(Arc::clone(&core), stream, |core, actions| { - async move { - core.rope.edit(actions); - } - .boxed() - }); - - // Create the binding - RopeBinding { core } - } - - /// - /// Creates a stream that follows the changes to this rope - /// - #[allow(dead_code)] - pub fn follow_changes(&self) -> RopeStream<Cell, Attribute> { - // Fetch an ID for the next stream from the core and generate a state - let stream_id = self.core.sync(|core| { - // Assign an ID to the stream - let next_id = core.next_stream_id; - core.next_stream_id += 1; - - // Create a state for this stream - let state = RopeStreamState { - identifier: next_id, - waker: None, - pending_changes: VecDeque::new(), - needs_pull: false, - }; - core.stream_states.push(state); - - // Return the stream ID - next_id - }); - - // Create the stream - RopeStream { - identifier: stream_id, - core: self.core.clone(), - poll_future: None, - draining: VecDeque::new(), - } - } - - /// - /// Returns the number of cells in this rope - /// - #[allow(dead_code)] - pub fn len(&self) -> usize { - self.core.sync(|core| core.rope.len()) - } - - /// - /// Reads the cell values for a range in this rope - /// - #[allow(dead_code)] - pub fn read_cells<'a>(&'a self, range: Range<usize>) -> impl 'a + Iterator<Item = Cell> { - // Read this range of cells by cloning from the core - let cells = self - .core - .sync(|core| core.rope.read_cells(range).cloned().collect::<Vec<_>>()); - - cells.into_iter() - } - - /// - /// Returns the attributes set at the specified location and their extent - /// - #[allow(dead_code)] - pub fn read_attributes<'a>(&'a self, pos: usize) -> (Attribute, Range<usize>) { - let (attribute, range) = self.core.sync(|core| { - let (attribute, range) = core.rope.read_attributes(pos); - (attribute.clone(), range) - }); - - (attribute, range) - } -} - -impl<Cell, Attribute> Clone for RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - fn clone(&self) -> RopeBinding<Cell, Attribute> { - // Increase the usage count - let core = self.core.clone(); - core.desync(|core| core.usage_count += 1); - - // Create a new binding with the same core - RopeBinding { core } - } -} - -impl<Cell, Attribute> Drop for RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - fn drop(&mut self) { - self.core.desync(|core| { - // Core is no longer in use - core.usage_count -= 1; - - // Counts as a notification if this is the last binding using this core - if core.usage_count == 0 { - core.pull_rope(); - } - }) - } -} - -impl<Cell, Attribute> Changeable for RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Supplies a function to be notified when this item is changed - /// - /// This event is only fired after the value has been read since the most recent - /// change. Note that this means if the value is never read, this event may - /// never fire. This behaviour is desirable when deferring updates as it prevents - /// large cascades of 'changed' events occurring for complicated dependency trees. - /// - /// The releasable that's returned has keep_alive turned off by default, so - /// be sure to store it in a variable or call keep_alive() to keep it around - /// (if the event never seems to fire, this is likely to be the problem) - /// - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - let releasable = ReleasableNotifiable::new(what); - let core_releasable = releasable.clone_as_owned(); - - self.core.desync(move |core| { - core.when_changed.push(core_releasable); - core.filter_unused_notifications(); - }); - - Box::new(releasable) - } -} - -/// -/// Trait implemented by something that is bound to a value -/// -impl<Cell, Attribute> Bound<AttributedRope<Cell, Attribute>> for RopeBinding<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Retrieves the value stored by this binding - /// - fn get(&self) -> AttributedRope<Cell, Attribute> { - self.core.sync(|core| { - // Create a new rope from the existing one - let mut rope_copy = AttributedRope::new(); - - // Copy each attribute block one at a time - let len = core.rope.len(); - let mut pos = 0; - - while pos < len { - // Read the next range of attributes - let (attr, range) = core.rope.read_attributes(pos); - if range.len() == 0 { - pos += 1; - continue; - } - - // Write to the copy - let attr = attr.clone(); - let cells = core.rope.read_cells(range.clone()).cloned(); - rope_copy.replace_attributes(pos..pos, cells, attr); - - // Continue writing at the end of the new rope - pos = range.end; - } - - rope_copy - }) - } -} diff --git a/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs b/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs deleted file mode 100644 index 87faf09..0000000 --- a/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs +++ /dev/null @@ -1,287 +0,0 @@ -use crate::flo_binding::releasable::*; -use crate::flo_binding::rope_binding::core::*; -use crate::flo_binding::rope_binding::stream::*; -use crate::flo_binding::rope_binding::stream_state::*; -use crate::flo_binding::traits::*; - -use ::desync::*; -use flo_rope::*; - -use std::collections::VecDeque; -use std::ops::Range; -use std::sync::*; - -/// -/// A rope binding binds a vector of cells and attributes -/// -/// A `RopeBindingMut` supplies the same functionality as a `RopeBinding` except it also provides the -/// editing functions for changing the underlying data. -/// -pub struct RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Unpin + Clone + PartialEq + Default, -{ - /// The core of this binding - core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, -} - -impl<Cell, Attribute> RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Creates a new rope binding from a stream of changes - /// - #[allow(dead_code)] - pub fn new() -> RopeBindingMut<Cell, Attribute> { - // Create the core - let core = RopeBindingCore { - usage_count: 1, - rope: PullRope::from(AttributedRope::new(), Box::new(|| {})), - stream_states: vec![], - next_stream_id: 0, - when_changed: vec![], - }; - - let core = Arc::new(Desync::new(core)); - - // Recreate the rope in the core with a version that responds to pull events - let weak_core = Arc::downgrade(&core); - core.sync(move |core| { - core.rope = PullRope::from( - AttributedRope::new(), - Box::new(move || { - // Pass the event through to the core - let core = weak_core.upgrade(); - if let Some(core) = core { - core.desync(|core| core.on_pull()); - } - }), - ); - }); - - // Create the binding - RopeBindingMut { core } - } - - /// - /// Creates a stream that follows the changes to this rope - /// - #[allow(dead_code)] - pub fn follow_changes(&self) -> RopeStream<Cell, Attribute> { - // Fetch an ID for the next stream from the core and generate a state - let stream_id = self.core.sync(|core| { - // Assign an ID to the stream - let next_id = core.next_stream_id; - core.next_stream_id += 1; - - // Create a state for this stream - let state = RopeStreamState { - identifier: next_id, - waker: None, - pending_changes: VecDeque::new(), - needs_pull: false, - }; - core.stream_states.push(state); - - // Return the stream ID - next_id - }); - - // Create the stream - RopeStream { - identifier: stream_id, - core: self.core.clone(), - poll_future: None, - draining: VecDeque::new(), - } - } - - /// - /// Returns the number of cells in this rope - /// - #[allow(dead_code)] - pub fn len(&self) -> usize { - self.core.sync(|core| core.rope.len()) - } - - /// - /// Reads the cell values for a range in this rope - /// - #[allow(dead_code)] - pub fn read_cells<'a>(&'a self, range: Range<usize>) -> impl 'a + Iterator<Item = Cell> { - // Read this range of cells by cloning from the core - let cells = self - .core - .sync(|core| core.rope.read_cells(range).cloned().collect::<Vec<_>>()); - - cells.into_iter() - } - - /// - /// Returns the attributes set at the specified location and their extent - /// - #[allow(dead_code)] - pub fn read_attributes<'a>(&'a self, pos: usize) -> (Attribute, Range<usize>) { - let (attribute, range) = self.core.sync(|core| { - let (attribute, range) = core.rope.read_attributes(pos); - (attribute.clone(), range) - }); - - (attribute, range) - } - - /// - /// Performs the specified editing action to this rope - /// - #[allow(dead_code)] - pub fn edit(&mut self, action: RopeAction<Cell, Attribute>) { - self.core.sync(move |core| core.rope.edit(action)); - } - - /// - /// Replaces a range of cells. The attributes applied to the new cells will be the same - /// as the attributes that were applied to the first cell in the replacement range - /// - #[allow(dead_code)] - pub fn replace<NewCells: 'static + Send + IntoIterator<Item = Cell>>( - &mut self, - range: Range<usize>, - new_cells: NewCells, - ) { - self.core - .sync(move |core| core.rope.replace(range, new_cells)); - } - - /// - /// Sets the attributes for a range of cells - /// - #[allow(dead_code)] - pub fn set_attributes(&mut self, range: Range<usize>, new_attributes: Attribute) { - self.core - .sync(move |core| core.rope.set_attributes(range, new_attributes)); - } - - /// - /// Replaces a range of cells and sets the attributes for them. - /// - #[allow(dead_code)] - pub fn replace_attributes<NewCells: 'static + Send + IntoIterator<Item = Cell>>( - &mut self, - range: Range<usize>, - new_cells: NewCells, - new_attributes: Attribute, - ) { - self.core.sync(move |core| { - core.rope - .replace_attributes(range, new_cells, new_attributes) - }); - } -} - -impl<Cell, Attribute> Clone for RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - fn clone(&self) -> RopeBindingMut<Cell, Attribute> { - // Increase the usage count - let core = self.core.clone(); - core.desync(|core| core.usage_count += 1); - - // Create a new binding with the same core - RopeBindingMut { core } - } -} - -impl<Cell, Attribute> Drop for RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - fn drop(&mut self) { - self.core.desync(|core| { - // Core is no longer in use - core.usage_count -= 1; - - // Counts as a notification if this is the last binding using this core - if core.usage_count == 0 { - core.pull_rope(); - } - }) - } -} - -impl<Cell, Attribute> Changeable for RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Supplies a function to be notified when this item is changed - /// - /// This event is only fired after the value has been read since the most recent - /// change. Note that this means if the value is never read, this event may - /// never fire. This behaviour is desirable when deferring updates as it prevents - /// large cascades of 'changed' events occurring for complicated dependency trees. - /// - /// The releasable that's returned has keep_alive turned off by default, so - /// be sure to store it in a variable or call keep_alive() to keep it around - /// (if the event never seems to fire, this is likely to be the problem) - /// - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { - let releasable = ReleasableNotifiable::new(what); - let core_releasable = releasable.clone_as_owned(); - - self.core.desync(move |core| { - core.when_changed.push(core_releasable); - core.filter_unused_notifications(); - }); - - Box::new(releasable) - } -} - -/// -/// Trait implemented by something that is bound to a value -/// -impl<Cell, Attribute> Bound<AttributedRope<Cell, Attribute>> for RopeBindingMut<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// - /// Retrieves the value stored by this binding - /// - fn get(&self) -> AttributedRope<Cell, Attribute> { - self.core.sync(|core| { - // Create a new rope from the existing one - let mut rope_copy = AttributedRope::new(); - - // Copy each attribute block one at a time - let len = core.rope.len(); - let mut pos = 0; - - while pos < len { - // Read the next range of attributes - let (attr, range) = core.rope.read_attributes(pos); - if range.len() == 0 { - pos += 1; - continue; - } - - // Write to the copy - let attr = attr.clone(); - let cells = core.rope.read_cells(range.clone()).cloned(); - rope_copy.replace_attributes(pos..pos, cells, attr); - - // Continue writing at the end of the new rope - pos = range.end; - } - - rope_copy - }) - } -} diff --git a/kayak_core/src/flo_binding/rope_binding/stream.rs b/kayak_core/src/flo_binding/rope_binding/stream.rs deleted file mode 100644 index 87d70cf..0000000 --- a/kayak_core/src/flo_binding/rope_binding/stream.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::flo_binding::rope_binding::core::*; - -use ::desync::*; -use flo_rope::*; -use futures::future::BoxFuture; -use futures::prelude::*; -use futures::task::*; - -use std::collections::VecDeque; -use std::mem; -use std::pin::*; -use std::sync::*; - -/// -/// A rope stream monitors a rope binding, and supplies them as a stream so they can be mirrored elsewhere -/// -/// An example of a use for a rope stream is to send updates from a rope to a user interface. -/// -pub struct RopeStream<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - /// The identifier for this stream - pub(super) identifier: usize, - - /// The core of the rope - pub(super) core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, - - /// A future that will return the next poll result - pub(super) poll_future: - Option<BoxFuture<'static, Poll<Option<VecDeque<RopeAction<Cell, Attribute>>>>>>, - - /// The actions that are currently being drained through this stream - pub(super) draining: VecDeque<RopeAction<Cell, Attribute>>, -} - -impl<Cell, Attribute> Stream for RopeStream<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - type Item = RopeAction<Cell, Attribute>; - - fn poll_next( - mut self: Pin<&mut Self>, - ctxt: &mut Context<'_>, - ) -> Poll<Option<RopeAction<Cell, Attribute>>> { - // If we've got a set of actions we're already reading, then return those as fast as we can - if self.draining.len() > 0 { - return Poll::Ready(self.draining.pop_back()); - } - - // If we're waiting for the core to return to us, borrow the future from there - let poll_future = self.poll_future.take(); - let mut poll_future = if let Some(poll_future) = poll_future { - // We're already waiting for the core to get back to us - poll_future - } else { - // Ask the core for the next stream state - let stream_id = self.identifier; - - self.core - .future_desync(move |core| { - async move { - // Pull any pending changes from the rope - core.pull_rope(); - - // Find the state of this stream - let stream_state = core - .stream_states - .iter_mut() - .filter(|state| state.identifier == stream_id) - .nth(0) - .unwrap(); - - // Check for data - if stream_state.pending_changes.len() > 0 { - // Return the changes to the waiting stream - let mut changes = VecDeque::new(); - mem::swap(&mut changes, &mut stream_state.pending_changes); - - Poll::Ready(Some(changes)) - } else if core.usage_count == 0 { - // No changes, and nothing is using the core any more - Poll::Ready(None) - } else { - // No changes are waiting - Poll::Pending - } - } - .boxed() - }) - .map(|result| { - // Error would indicate the core had gone away before the request should complete, so we signal this as an end-of-stream event - match result { - Ok(result) => result, - Err(_) => Poll::Ready(None), - } - }) - .boxed() - }; - - // Ask the future for the latest update on this stream - let future_result = poll_future.poll_unpin(ctxt); - - match future_result { - Poll::Ready(Poll::Ready(Some(actions))) => { - if actions.len() == 0 { - // Nothing waiting: need to wait until the rope signals a 'pull' event - let waker = ctxt.waker().clone(); - let stream_id = self.identifier; - - self.core.desync(move |core| { - core.wake_stream(stream_id, waker); - }); - - Poll::Pending - } else { - // Have some actions ready - self.draining = actions; - Poll::Ready(self.draining.pop_back()) - } - } - - Poll::Ready(Poll::Ready(None)) => Poll::Ready(None), - Poll::Ready(Poll::Pending) => { - // Wake when the rope generates a 'pull' event - let waker = ctxt.waker().clone(); - let stream_id = self.identifier; - - self.core.desync(move |core| { - core.wake_stream(stream_id, waker); - }); - - Poll::Pending - } - - Poll::Pending => { - // Poll the future again when it notifies - self.poll_future = Some(poll_future); - Poll::Pending - } - } - } -} - -impl<Cell, Attribute> Drop for RopeStream<Cell, Attribute> -where - Cell: 'static + Send + Unpin + Clone + PartialEq, - Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, -{ - fn drop(&mut self) { - // Remove the stream state when the stream is no more - let dropped_stream_id = self.identifier; - self.core.desync(move |core| { - core.stream_states - .retain(|state| state.identifier != dropped_stream_id); - }); - } -} diff --git a/kayak_core/src/flo_binding/rope_binding/stream_state.rs b/kayak_core/src/flo_binding/rope_binding/stream_state.rs deleted file mode 100644 index b0114ea..0000000 --- a/kayak_core/src/flo_binding/rope_binding/stream_state.rs +++ /dev/null @@ -1,25 +0,0 @@ -use flo_rope::*; -use futures::task::*; - -use std::collections::VecDeque; - -/// -/// The state of a stream that is reading from a rope binding core -/// -pub(super) struct RopeStreamState<Cell, Attribute> -where - Cell: Clone + PartialEq, - Attribute: Clone + PartialEq + Default, -{ - /// The identifier for this stream - pub(super) identifier: usize, - - /// The waker for the current stream - pub(super) waker: Option<Waker>, - - /// The changes that are waiting to be sent to this stream - pub(super) pending_changes: VecDeque<RopeAction<Cell, Attribute>>, - - /// True if the rope has indicated there are changes waiting to be pulled - pub(super) needs_pull: bool, -} diff --git a/kayak_core/src/flo_binding/rope_binding/tests.rs b/kayak_core/src/flo_binding/rope_binding/tests.rs deleted file mode 100644 index e28a6e1..0000000 --- a/kayak_core/src/flo_binding/rope_binding/tests.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::flo_binding::*; - -use flo_rope::*; - -use futures::executor; -use futures::prelude::*; - -#[test] -fn mutable_rope_sends_changes_to_stream() { - // Create a rope that copies changes from a mutable rope - let mut mutable_rope = RopeBindingMut::<usize, ()>::new(); - let mut rope_stream = mutable_rope.follow_changes(); - - // Write some data to the mutable rope - mutable_rope.replace(0..0, vec![1, 2, 3, 4]); - - // Should get sent to the stream - executor::block_on(async move { - let next = rope_stream.next().await; - - assert!(next == Some(RopeAction::Replace(0..0, vec![1, 2, 3, 4]))); - }); -} - -#[test] -fn pull_from_mutable_binding() { - // Create a rope that copies changes from a mutable rope - let mut mutable_rope = RopeBindingMut::<usize, ()>::new(); - let rope_copy = RopeBinding::from_stream(mutable_rope.follow_changes()); - let mut rope_stream = rope_copy.follow_changes(); - - // Write some data to the mutable rope - mutable_rope.replace(0..0, vec![1, 2, 3, 4]); - - // Wait for the change to arrive at the copy - executor::block_on(async move { - let next = rope_stream.next().await; - assert!(next == Some(RopeAction::Replace(0..0, vec![1, 2, 3, 4]))) - }); - - // Read from the copy - assert!(rope_copy.len() == 4); - assert!(rope_copy.read_cells(0..4).collect::<Vec<_>>() == vec![1, 2, 3, 4]); -} diff --git a/kayak_core/src/flo_binding/traits.rs b/kayak_core/src/flo_binding/traits.rs deleted file mode 100644 index 5b3bbec..0000000 --- a/kayak_core/src/flo_binding/traits.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::sync::*; - -/// -/// Trait implemented by items with dependencies that need to be notified when they have changed -/// -pub trait Notifiable: Sync + Send { - /// - /// Indicates that a dependency of this object has changed - /// - fn mark_as_changed(&self); -} - -/// -/// Trait implemented by an object that can be released: for example to stop performing -/// an action when it's no longer required. -/// -pub trait Releasable: Send + Sync { - /// - /// Indicates that this object should not be released on drop - /// - fn keep_alive(&mut self); - - /// - /// Indicates that this object is finished with and should be released - /// - fn done(&mut self); -} - -/// -/// Trait implemented by items that can notify something when they're changed -/// -pub trait Changeable { - /// - /// Supplies a function to be notified when this item is changed - /// - /// This event is only fired after the value has been read since the most recent - /// change. Note that this means if the value is never read, this event may - /// never fire. This behaviour is desirable when deferring updates as it prevents - /// large cascades of 'changed' events occurring for complicated dependency trees. - /// - /// The releasable that's returned has keep_alive turned off by default, so - /// be sure to store it in a variable or call keep_alive() to keep it around - /// (if the event never seems to fire, this is likely to be the problem) - /// - fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable>; -} - -/// -/// Trait implemented by something that is bound to a value -/// -pub trait Bound<Value>: Changeable + Send + Sync { - /// - /// Retrieves the value stored by this binding - /// - fn get(&self) -> Value; -} - -/// -/// Trait implemented by something that is bound to a value -/// -// Seperate Trait to allow Bound to be made into an object for BindRef -pub trait WithBound<Value>: Changeable + Send + Sync { - /// - /// Mutate instead of replacing value stored in this binding, return true - /// to send notifiations - /// - fn with_ref<F, T>(&self, f: F) -> T - where - F: FnOnce(&Value) -> T; - /// - /// Mutate instead of replacing value stored in this binding, return true - /// to send notifiations - /// - fn with_mut<F>(&self, f: F) - where - F: FnOnce(&mut Value) -> bool; -} -/// -/// Trait implemented by something that is bound to a value that can be changed -/// -/// Bindings are similar in behaviour to Arc<Mutex<Value>>, so it's possible to set -/// the value of their target even when the binding itself is not mutable. -/// -pub trait MutableBound<Value>: Bound<Value> { - /// - /// Sets the value stored by this binding - /// - fn set(&self, new_value: Value); -} diff --git a/kayak_core/src/fragment.rs b/kayak_core/src/fragment.rs deleted file mode 100644 index 1cb454c..0000000 --- a/kayak_core/src/fragment.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::{ - context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, OnLayout, Widget, - WidgetProps, -}; - -/// Props used by the [`Fragment`] widget -#[derive(Default, Debug, PartialEq, Clone)] -pub struct FragmentProps { - pub styles: Option<Style>, - pub children: Option<crate::Children>, -} - -/// The base widget, used to actually build and render children -/// -/// # Props -/// -/// __Type:__ [`FragmentProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ⌠| -/// | `focusable` | ⌠| -/// -#[derive(Default, Debug, PartialEq, Clone)] -pub struct Fragment { - pub id: Index, - props: FragmentProps, -} - -impl WidgetProps for FragmentProps { - fn get_children(&self) -> Option<Children> { - self.children.clone() - } - - fn set_children(&mut self, children: Option<Children>) { - self.children = children; - } - - fn get_styles(&self) -> Option<Style> { - self.styles.clone() - } - - fn get_on_event(&self) -> Option<OnEvent> { - None - } - - fn get_on_layout(&self) -> Option<OnLayout> { - None - } - - fn get_focusable(&self) -> Option<bool> { - Some(false) - } -} - -impl Widget for Fragment { - type Props = FragmentProps; - - fn constructor(props: Self::Props) -> Self - where - Self: Sized, - { - Self { - id: Index::default(), - props, - } - } - - fn get_id(&self) -> Index { - self.id - } - - fn set_id(&mut self, id: Index) { - self.id = id; - } - - fn get_props(&self) -> &Self::Props { - &self.props - } - - fn get_props_mut(&mut self) -> &mut Self::Props { - &mut self.props - } - - fn render(&mut self, context: &mut KayakContextRef) { - let parent_id = self.get_id(); - if let Some(children) = self.props.children.take() { - let mut context = KayakContextRef::new(&mut context.context, Some(parent_id)); - children.build(Some(parent_id), &mut context); - } else { - return; - } - - // Note: No need to do anything here with this KayakContextRef. - } -} diff --git a/kayak_core/src/generational_arena.rs b/kayak_core/src/generational_arena.rs deleted file mode 100644 index bd3bb4e..0000000 --- a/kayak_core/src/generational_arena.rs +++ /dev/null @@ -1,1323 +0,0 @@ -/*! -[](https://docs.rs/generational-arena/) -[](https://crates.io/crates/generational-arena) -[](https://crates.io/crates/generational-arena) -[](https://travis-ci.org/fitzgen/generational-arena) -A safe arena allocator that allows deletion without suffering from [the ABA -problem](https://en.wikipedia.org/wiki/ABA_problem) by using generational -indices. -Inspired by [Catherine West's closing keynote at RustConf -2018](https://www.youtube.com/watch?v=aKLntZcp27M), where these ideas (and many -more!) were presented in the context of an Entity-Component-System for games -programming. -## What? Why? -Imagine you are working with a graph and you want to add and delete individual -nodes at a time, or you are writing a game and its world consists of many -inter-referencing objects with dynamic lifetimes that depend on user -input. These are situations where matching Rust's ownership and lifetime rules -can get tricky. -It doesn't make sense to use shared ownership with interior mutability (i.e. -`Rc<RefCell<T>>` or `Arc<Mutex<T>>`) nor borrowed references (ie `&'a T` or `&'a -mut T`) for structures. The cycles rule out reference counted types, and the -required shared mutability rules out borrows. Furthermore, lifetimes are dynamic -and don't follow the borrowed-data-outlives-the-borrower discipline. -In these situations, it is tempting to store objects in a `Vec<T>` and have them -reference each other via their indices. No more borrow checker or ownership -problems! Often, this solution is good enough. -However, now we can't delete individual items from that `Vec<T>` when we no -longer need them, because we end up either -* messing up the indices of every element that follows the deleted one, or -* suffering from the [ABA - problem](https://en.wikipedia.org/wiki/ABA_problem). To elaborate further, if - we tried to replace the `Vec<T>` with a `Vec<Option<T>>`, and delete an - element by setting it to `None`, then we create the possibility for this buggy - sequence: - * `obj1` references `obj2` at index `i` - * someone else deletes `obj2` from index `i`, setting that element to `None` - * a third thing allocates `obj3`, which ends up at index `i`, because the - element at that index is `None` and therefore available for allocation - * `obj1` attempts to get `obj2` at index `i`, but incorrectly is given - `obj3`, when instead the get should fail. -By introducing a monotonically increasing generation counter to the collection, -associating each element in the collection with the generation when it was -inserted, and getting elements from the collection with the *pair* of index and -the generation at the time when the element was inserted, then we can solve the -aforementioned ABA problem. When indexing into the collection, if the index -pair's generation does not match the generation of the element at that index, -then the operation fails. -## Features -* Zero `unsafe` -* Well tested, including quickchecks -* `no_std` compatibility -* All the trait implementations you expect: `IntoIterator`, `FromIterator`, - `Extend`, etc... -## Usage -First, add `generational-arena` to your `Cargo.toml`: -```toml -[dependencies] -generational-arena = "0.2" -``` -Then, import the crate and use the -[`generational_arena::Arena`](./struct.Arena.html) type! -```rust -extern crate generational_arena; -use generational_arena::Arena; -let mut arena = Arena::new(); -// Insert some elements into the arena. -let rza = arena.insert("Robert Fitzgerald Diggs"); -let gza = arena.insert("Gary Grice"); -let bill = arena.insert("Bill Gates"); -// Inserted elements can be accessed infallibly via indexing (and missing -// entries will panic). -assert_eq!(arena[rza], "Robert Fitzgerald Diggs"); -// Alternatively, the `get` and `get_mut` methods provide fallible lookup. -if let Some(genius) = arena.get(gza) { - println!("The gza gza genius: {}", genius); -} -if let Some(val) = arena.get_mut(bill) { - *val = "Bill Gates doesn't belong in this set..."; -} -// We can remove elements. -arena.remove(bill); -// Insert a new one. -let murray = arena.insert("Bill Murray"); -// The arena does not contain `bill` anymore, but it does contain `murray`, even -// though they are almost certainly at the same index within the arena in -// practice. Ambiguities are resolved with an associated generation tag. -assert!(!arena.contains(bill)); -assert!(arena.contains(murray)); -// Iterate over everything inside the arena. -for (idx, value) in &arena { - println!("{:?} is at {:?}", value, idx); -} -``` -## `no_std` -To enable `no_std` compatibility, disable the on-by-default "std" feature. -```toml -[dependencies] -generational-arena = { version = "0.2", default-features = false } -``` -### Serialization and Deserialization with [`serde`](https://crates.io/crates/serde) -To enable serialization/deserialization support, enable the "serde" feature. -```toml -[dependencies] -generational-arena = { version = "0.2", features = ["serde"] } -``` - */ - -#![forbid(unsafe_code, missing_docs, missing_debug_implementations)] - -use std::vec::{self, Vec}; - -use core::cmp; -use core::iter::{self, Extend, FromIterator, FusedIterator}; -use core::mem; -use core::ops; -use core::slice; - -/// The `Arena` allows inserting and removing elements that are referred to by -/// `Index`. -/// -/// [See the module-level documentation for example usage and motivation.](./index.html) -#[derive(Clone, Debug)] -pub struct Arena<T> { - items: Vec<Entry<T>>, - generation: u64, - free_list_head: Option<usize>, - len: usize, -} - -#[derive(Clone, Debug)] -enum Entry<T> { - Free { next_free: Option<usize> }, - Occupied { generation: u64, value: T }, -} - -/// An index (and generation) into an `Arena`. -/// -/// To get an `Index`, insert an element into an `Arena`, and the `Index` for -/// that element will be returned. -/// -/// # Examples -/// -/// ``` -/// use generational_arena::Arena; -/// -/// let mut arena = Arena::new(); -/// let idx = arena.insert(123); -/// assert_eq!(arena[idx], 123); -/// ``` -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Index { - index: usize, - generation: u64, -} - -impl Index { - /// Create a new `Index` from its raw parts. - /// - /// The parts must have been returned from an earlier call to - /// `into_raw_parts`. - /// - /// Providing arbitrary values will lead to malformed indices and ultimately - /// panics. - pub fn from_raw_parts(a: usize, b: u64) -> Index { - Index { - index: a, - generation: b, - } - } - - /// Convert this `Index` into its raw parts. - /// - /// This niche method is useful for converting an `Index` into another - /// identifier type. Usually, you should prefer a newtype wrapper around - /// `Index` like `pub struct MyIdentifier(Index);`. However, for external - /// types whose definition you can't customize, but which you can construct - /// instances of, this method can be useful. - pub fn into_raw_parts(self) -> (usize, u64) { - (self.index, self.generation) - } -} - -const DEFAULT_CAPACITY: usize = 4; - -impl<T> Default for Arena<T> { - fn default() -> Arena<T> { - Arena::new() - } -} - -impl<T> Arena<T> { - /// Constructs a new, empty `Arena`. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::<usize>::new(); - /// # let _ = arena; - /// ``` - pub fn new() -> Arena<T> { - Arena::with_capacity(DEFAULT_CAPACITY) - } - - /// Constructs a new, empty `Arena<T>` with the specified capacity. - /// - /// The `Arena<T>` will be able to hold `n` elements without further allocation. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::with_capacity(10); - /// - /// // These insertions will not require further allocation. - /// for i in 0..10 { - /// assert!(arena.try_insert(i).is_ok()); - /// } - /// - /// // But now we are at capacity, and there is no more room. - /// assert!(arena.try_insert(99).is_err()); - /// ``` - pub fn with_capacity(n: usize) -> Arena<T> { - let n = cmp::max(n, 1); - let mut arena = Arena { - items: Vec::new(), - generation: 0, - free_list_head: None, - len: 0, - }; - arena.reserve(n); - arena - } - - /// Clear all the items inside the arena, but keep its allocation. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::with_capacity(1); - /// arena.insert(42); - /// arena.insert(43); - /// - /// arena.clear(); - /// - /// assert_eq!(arena.capacity(), 2); - /// ``` - pub fn clear(&mut self) { - self.items.clear(); - - let end = self.items.capacity(); - self.items.extend((0..end).map(|i| { - if i == end - 1 { - Entry::Free { next_free: None } - } else { - Entry::Free { - next_free: Some(i + 1), - } - } - })); - if !self.is_empty() { - // Increment generation, but if there are no elements, do nothing to - // avoid unnecessary incrementing generation. - self.generation += 1; - } - self.free_list_head = Some(0); - self.len = 0; - } - - /// Attempts to insert `value` into the arena using existing capacity. - /// - /// This method will never allocate new capacity in the arena. - /// - /// If insertion succeeds, then the `value`'s index is returned. If - /// insertion fails, then `Err(value)` is returned to give ownership of - /// `value` back to the caller. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// - /// match arena.try_insert(42) { - /// Ok(idx) => { - /// // Insertion succeeded. - /// assert_eq!(arena[idx], 42); - /// } - /// Err(x) => { - /// // Insertion failed. - /// assert_eq!(x, 42); - /// } - /// }; - /// ``` - #[inline] - pub fn try_insert(&mut self, value: T) -> Result<Index, T> { - match self.try_alloc_next_index() { - None => Err(value), - Some(index) => { - self.items[index.index] = Entry::Occupied { - generation: self.generation, - value, - }; - Ok(index) - } - } - } - - /// Attempts to insert the value returned by `create` into the arena using existing capacity. - /// `create` is called with the new value's associated index, allowing values that know their own index. - /// - /// This method will never allocate new capacity in the arena. - /// - /// If insertion succeeds, then the new index is returned. If - /// insertion fails, then `Err(create)` is returned to give ownership of - /// `create` back to the caller. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::{Arena, Index}; - /// - /// let mut arena = Arena::new(); - /// - /// match arena.try_insert_with(|idx| (42, idx)) { - /// Ok(idx) => { - /// // Insertion succeeded. - /// assert_eq!(arena[idx].0, 42); - /// assert_eq!(arena[idx].1, idx); - /// } - /// Err(x) => { - /// // Insertion failed. - /// } - /// }; - /// ``` - #[inline] - pub fn try_insert_with<F: FnOnce(Index) -> T>(&mut self, create: F) -> Result<Index, F> { - match self.try_alloc_next_index() { - None => Err(create), - Some(index) => { - self.items[index.index] = Entry::Occupied { - generation: self.generation, - value: create(index), - }; - Ok(index) - } - } - } - - #[inline] - fn try_alloc_next_index(&mut self) -> Option<Index> { - match self.free_list_head { - None => None, - Some(i) => match self.items[i] { - Entry::Occupied { .. } => panic!("corrupt free list"), - Entry::Free { next_free } => { - self.free_list_head = next_free; - self.len += 1; - Some(Index { - index: i, - generation: self.generation, - }) - } - }, - } - } - - /// Insert `value` into the arena, allocating more capacity if necessary. - /// - /// The `value`'s associated index in the arena is returned. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// - /// let idx = arena.insert(42); - /// assert_eq!(arena[idx], 42); - /// ``` - #[inline] - pub fn insert(&mut self, value: T) -> Index { - match self.try_insert(value) { - Ok(i) => i, - Err(value) => self.insert_slow_path(value), - } - } - - /// Insert the value returned by `create` into the arena, allocating more capacity if necessary. - /// `create` is called with the new value's associated index, allowing values that know their own index. - /// - /// The new value's associated index in the arena is returned. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::{Arena, Index}; - /// - /// let mut arena = Arena::new(); - /// - /// let idx = arena.insert_with(|idx| (42, idx)); - /// assert_eq!(arena[idx].0, 42); - /// assert_eq!(arena[idx].1, idx); - /// ``` - #[inline] - pub fn insert_with(&mut self, create: impl FnOnce(Index) -> T) -> Index { - match self.try_insert_with(create) { - Ok(i) => i, - Err(create) => self.insert_with_slow_path(create), - } - } - - #[inline(never)] - fn insert_slow_path(&mut self, value: T) -> Index { - let len = if self.capacity() == 0 { - // `drain()` sets the capacity to 0 and if the capacity is 0, the - // next `try_insert() `will refer to an out-of-range index because - // the next `reserve()` does not add element, resulting in a panic. - // So ensure that `self` have at least 1 capacity here. - // - // Ideally, this problem should be handled within `drain()`,but - // this problem cannot be handled within `drain()` because `drain()` - // returns an iterator that borrows `self` mutably. - 1 - } else { - self.items.len() - }; - self.reserve(len); - self.try_insert(value) - .map_err(|_| ()) - .expect("inserting will always succeed after reserving additional space") - } - - #[inline(never)] - fn insert_with_slow_path(&mut self, create: impl FnOnce(Index) -> T) -> Index { - let len = self.items.len(); - self.reserve(len); - self.try_insert_with(create) - .map_err(|_| ()) - .expect("inserting will always succeed after reserving additional space") - } - - /// Remove the element at index `i` from the arena. - /// - /// If the element at index `i` is still in the arena, then it is - /// returned. If it is not in the arena, then `None` is returned. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx = arena.insert(42); - /// - /// assert_eq!(arena.remove(idx), Some(42)); - /// assert_eq!(arena.remove(idx), None); - /// ``` - pub fn remove(&mut self, i: Index) -> Option<T> { - if i.index >= self.items.len() { - return None; - } - - match self.items[i.index] { - Entry::Occupied { generation, .. } if i.generation == generation => { - let entry = mem::replace( - &mut self.items[i.index], - Entry::Free { - next_free: self.free_list_head, - }, - ); - self.generation += 1; - self.free_list_head = Some(i.index); - self.len -= 1; - - match entry { - Entry::Occupied { - generation: _, - value, - } => Some(value), - _ => unreachable!(), - } - } - _ => None, - } - } - - /// Retains only the elements specified by the predicate. - /// - /// In other words, remove all indices such that `predicate(index, &value)` returns `false`. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut crew = Arena::new(); - /// crew.extend(&["Jim Hawkins", "John Silver", "Alexander Smollett", "Israel Hands"]); - /// let pirates = ["John Silver", "Israel Hands"]; // too dangerous to keep them around - /// crew.retain(|_index, member| !pirates.contains(member)); - /// let mut crew_members = crew.iter().map(|(_, member)| **member); - /// assert_eq!(crew_members.next(), Some("Jim Hawkins")); - /// assert_eq!(crew_members.next(), Some("Alexander Smollett")); - /// assert!(crew_members.next().is_none()); - /// ``` - pub fn retain(&mut self, mut predicate: impl FnMut(Index, &mut T) -> bool) { - for i in 0..self.capacity() { - let remove = match &mut self.items[i] { - Entry::Occupied { generation, value } => { - let index = Index { - index: i, - generation: *generation, - }; - if predicate(index, value) { - None - } else { - Some(index) - } - } - - _ => None, - }; - if let Some(index) = remove { - self.remove(index); - } - } - } - - /// Is the element at index `i` in the arena? - /// - /// Returns `true` if the element at `i` is in the arena, `false` otherwise. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx = arena.insert(42); - /// - /// assert!(arena.contains(idx)); - /// arena.remove(idx); - /// assert!(!arena.contains(idx)); - /// ``` - pub fn contains(&self, i: Index) -> bool { - self.get(i).is_some() - } - - /// Get a shared reference to the element at index `i` if it is in the - /// arena. - /// - /// If the element at index `i` is not in the arena, then `None` is returned. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx = arena.insert(42); - /// - /// assert_eq!(arena.get(idx), Some(&42)); - /// arena.remove(idx); - /// assert!(arena.get(idx).is_none()); - /// ``` - pub fn get(&self, i: Index) -> Option<&T> { - match self.items.get(i.index) { - Some(Entry::Occupied { generation, value }) if *generation == i.generation => { - Some(value) - } - _ => None, - } - } - - /// Get an exclusive reference to the element at index `i` if it is in the - /// arena. - /// - /// If the element at index `i` is not in the arena, then `None` is returned. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx = arena.insert(42); - /// - /// *arena.get_mut(idx).unwrap() += 1; - /// assert_eq!(arena.remove(idx), Some(43)); - /// assert!(arena.get_mut(idx).is_none()); - /// ``` - pub fn get_mut(&mut self, i: Index) -> Option<&mut T> { - match self.items.get_mut(i.index) { - Some(Entry::Occupied { generation, value }) if *generation == i.generation => { - Some(value) - } - _ => None, - } - } - - /// Get a pair of exclusive references to the elements at index `i1` and `i2` if it is in the - /// arena. - /// - /// If the element at index `i1` or `i2` is not in the arena, then `None` is returned for this - /// element. - /// - /// # Panics - /// - /// Panics if `i1` and `i2` are pointing to the same item of the arena. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx1 = arena.insert(0); - /// let idx2 = arena.insert(1); - /// - /// { - /// let (item1, item2) = arena.get2_mut(idx1, idx2); - /// - /// *item1.unwrap() = 3; - /// *item2.unwrap() = 4; - /// } - /// - /// assert_eq!(arena[idx1], 3); - /// assert_eq!(arena[idx2], 4); - /// ``` - pub fn get2_mut(&mut self, i1: Index, i2: Index) -> (Option<&mut T>, Option<&mut T>) { - let len = self.items.len(); - - if i1.index == i2.index { - assert!(i1.generation != i2.generation); - - if i1.generation > i2.generation { - return (self.get_mut(i1), None); - } - return (None, self.get_mut(i2)); - } - - if i1.index >= len { - return (None, self.get_mut(i2)); - } else if i2.index >= len { - return (self.get_mut(i1), None); - } - - let (raw_item1, raw_item2) = { - let (xs, ys) = self.items.split_at_mut(cmp::max(i1.index, i2.index)); - if i1.index < i2.index { - (&mut xs[i1.index], &mut ys[0]) - } else { - (&mut ys[0], &mut xs[i2.index]) - } - }; - - let item1 = match raw_item1 { - Entry::Occupied { generation, value } if *generation == i1.generation => Some(value), - _ => None, - }; - - let item2 = match raw_item2 { - Entry::Occupied { generation, value } if *generation == i2.generation => Some(value), - _ => None, - }; - - (item1, item2) - } - - /// Get the length of this arena. - /// - /// The length is the number of elements the arena holds. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// assert_eq!(arena.len(), 0); - /// - /// let idx = arena.insert(42); - /// assert_eq!(arena.len(), 1); - /// - /// let _ = arena.insert(0); - /// assert_eq!(arena.len(), 2); - /// - /// assert_eq!(arena.remove(idx), Some(42)); - /// assert_eq!(arena.len(), 1); - /// ``` - pub fn len(&self) -> usize { - self.len - } - - /// Returns true if the arena contains no elements - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// assert!(arena.is_empty()); - /// - /// let idx = arena.insert(42); - /// assert!(!arena.is_empty()); - /// - /// assert_eq!(arena.remove(idx), Some(42)); - /// assert!(arena.is_empty()); - /// ``` - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - /// Get the capacity of this arena. - /// - /// The capacity is the maximum number of elements the arena can hold - /// without further allocation, including however many it currently - /// contains. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::with_capacity(10); - /// assert_eq!(arena.capacity(), 10); - /// - /// // `try_insert` does not allocate new capacity. - /// for i in 0..10 { - /// assert!(arena.try_insert(1).is_ok()); - /// assert_eq!(arena.capacity(), 10); - /// } - /// - /// // But `insert` will if the arena is already at capacity. - /// arena.insert(0); - /// assert!(arena.capacity() > 10); - /// ``` - pub fn capacity(&self) -> usize { - self.items.len() - } - - /// Allocate space for `additional_capacity` more elements in the arena. - /// - /// # Panics - /// - /// Panics if this causes the capacity to overflow. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::with_capacity(10); - /// arena.reserve(5); - /// assert_eq!(arena.capacity(), 15); - /// # let _: Arena<usize> = arena; - /// ``` - pub fn reserve(&mut self, additional_capacity: usize) { - let start = self.items.len(); - let end = self.items.len() + additional_capacity; - let old_head = self.free_list_head; - self.items.reserve_exact(additional_capacity); - self.items.extend((start..end).map(|i| { - if i == end - 1 { - Entry::Free { - next_free: old_head, - } - } else { - Entry::Free { - next_free: Some(i + 1), - } - } - })); - self.free_list_head = Some(start); - } - - /// Iterate over shared references to the elements in this arena. - /// - /// Yields pairs of `(Index, &T)` items. - /// - /// Order of iteration is not defined. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// for i in 0..10 { - /// arena.insert(i * i); - /// } - /// - /// for (idx, value) in arena.iter() { - /// println!("{} is at index {:?}", value, idx); - /// } - /// ``` - pub fn iter(&self) -> Iter<T> { - Iter { - len: self.len, - inner: self.items.iter().enumerate(), - } - } - - /// Iterate over exclusive references to the elements in this arena. - /// - /// Yields pairs of `(Index, &mut T)` items. - /// - /// Order of iteration is not defined. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// for i in 0..10 { - /// arena.insert(i * i); - /// } - /// - /// for (_idx, value) in arena.iter_mut() { - /// *value += 5; - /// } - /// ``` - pub fn iter_mut(&mut self) -> IterMut<T> { - IterMut { - len: self.len, - inner: self.items.iter_mut().enumerate(), - } - } - - /// Iterate over elements of the arena and remove them. - /// - /// Yields pairs of `(Index, T)` items. - /// - /// Order of iteration is not defined. - /// - /// Note: All elements are removed even if the iterator is only partially consumed or not consumed at all. - /// - /// # Examples - /// - /// ``` - /// use generational_arena::Arena; - /// - /// let mut arena = Arena::new(); - /// let idx_1 = arena.insert("hello"); - /// let idx_2 = arena.insert("world"); - /// - /// assert!(arena.get(idx_1).is_some()); - /// assert!(arena.get(idx_2).is_some()); - /// for (idx, value) in arena.drain() { - /// assert!((idx == idx_1 && value == "hello") || (idx == idx_2 && value == "world")); - /// } - /// assert!(arena.get(idx_1).is_none()); - /// assert!(arena.get(idx_2).is_none()); - /// ``` - pub fn drain(&mut self) -> Drain<T> { - let old_len = self.len; - if !self.is_empty() { - // Increment generation, but if there are no elements, do nothing to - // avoid unnecessary incrementing generation. - self.generation += 1; - } - self.free_list_head = None; - self.len = 0; - Drain { - len: old_len, - inner: self.items.drain(..).enumerate(), - } - } - - /// Given an i of `usize` without a generation, get a shared reference - /// to the element and the matching `Index` of the entry behind `i`. - /// - /// This method is useful when you know there might be an element at the - /// position i, but don't know its generation or precise Index. - /// - /// Use cases include using indexing such as Hierarchical BitMap Indexing or - /// other kinds of bit-efficient indexing. - /// - /// You should use the `get` method instead most of the time. - pub fn get_unknown_gen(&self, i: usize) -> Option<(&T, Index)> { - match self.items.get(i) { - Some(Entry::Occupied { generation, value }) => Some(( - value, - Index { - generation: *generation, - index: i, - }, - )), - _ => None, - } - } - - /// Given an i of `usize` without a generation, get an exclusive reference - /// to the element and the matching `Index` of the entry behind `i`. - /// - /// This method is useful when you know there might be an element at the - /// position i, but don't know its generation or precise Index. - /// - /// Use cases include using indexing such as Hierarchical BitMap Indexing or - /// other kinds of bit-efficient indexing. - /// - /// You should use the `get_mut` method instead most of the time. - pub fn get_unknown_gen_mut(&mut self, i: usize) -> Option<(&mut T, Index)> { - match self.items.get_mut(i) { - Some(Entry::Occupied { generation, value }) => Some(( - value, - Index { - generation: *generation, - index: i, - }, - )), - _ => None, - } - } -} - -impl<T> IntoIterator for Arena<T> { - type Item = T; - type IntoIter = IntoIter<T>; - fn into_iter(self) -> Self::IntoIter { - IntoIter { - len: self.len, - inner: self.items.into_iter(), - } - } -} - -/// An iterator over the elements in an arena. -/// -/// Yields `T` items. -/// -/// Order of iteration is not defined. -/// -/// # Examples -/// -/// ``` -/// use generational_arena::Arena; -/// -/// let mut arena = Arena::new(); -/// for i in 0..10 { -/// arena.insert(i * i); -/// } -/// -/// for value in arena { -/// assert!(value < 100); -/// } -/// ``` -#[derive(Clone, Debug)] -pub struct IntoIter<T> { - len: usize, - inner: vec::IntoIter<Entry<T>>, -} - -impl<T> Iterator for IntoIter<T> { - type Item = T; - - fn next(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next() { - Some(Entry::Free { .. }) => continue, - Some(Entry::Occupied { value, .. }) => { - self.len -= 1; - return Some(value); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - (self.len, Some(self.len)) - } -} - -impl<T> DoubleEndedIterator for IntoIter<T> { - fn next_back(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next_back() { - Some(Entry::Free { .. }) => continue, - Some(Entry::Occupied { value, .. }) => { - self.len -= 1; - return Some(value); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } -} - -impl<T> ExactSizeIterator for IntoIter<T> { - fn len(&self) -> usize { - self.len - } -} - -impl<T> FusedIterator for IntoIter<T> {} - -impl<'a, T> IntoIterator for &'a Arena<T> { - type Item = (Index, &'a T); - type IntoIter = Iter<'a, T>; - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -/// An iterator over shared references to the elements in an arena. -/// -/// Yields pairs of `(Index, &T)` items. -/// -/// Order of iteration is not defined. -/// -/// # Examples -/// -/// ``` -/// use generational_arena::Arena; -/// -/// let mut arena = Arena::new(); -/// for i in 0..10 { -/// arena.insert(i * i); -/// } -/// -/// for (idx, value) in &arena { -/// println!("{} is at index {:?}", value, idx); -/// } -/// ``` -#[derive(Clone, Debug)] -pub struct Iter<'a, T: 'a> { - len: usize, - inner: iter::Enumerate<slice::Iter<'a, Entry<T>>>, -} - -impl<'a, T> Iterator for Iter<'a, T> { - type Item = (Index, &'a T); - - fn next(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next() { - Some((_, &Entry::Free { .. })) => continue, - Some(( - index, - &Entry::Occupied { - generation, - ref value, - }, - )) => { - self.len -= 1; - let idx = Index { index, generation }; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - (self.len, Some(self.len)) - } -} - -impl<'a, T> DoubleEndedIterator for Iter<'a, T> { - fn next_back(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next_back() { - Some((_, &Entry::Free { .. })) => continue, - Some(( - index, - &Entry::Occupied { - generation, - ref value, - }, - )) => { - self.len -= 1; - let idx = Index { index, generation }; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } -} - -impl<'a, T> ExactSizeIterator for Iter<'a, T> { - fn len(&self) -> usize { - self.len - } -} - -impl<'a, T> FusedIterator for Iter<'a, T> {} - -impl<'a, T> IntoIterator for &'a mut Arena<T> { - type Item = (Index, &'a mut T); - type IntoIter = IterMut<'a, T>; - fn into_iter(self) -> Self::IntoIter { - self.iter_mut() - } -} - -/// An iterator over exclusive references to elements in this arena. -/// -/// Yields pairs of `(Index, &mut T)` items. -/// -/// Order of iteration is not defined. -/// -/// # Examples -/// -/// ``` -/// use generational_arena::Arena; -/// -/// let mut arena = Arena::new(); -/// for i in 0..10 { -/// arena.insert(i * i); -/// } -/// -/// for (_idx, value) in &mut arena { -/// *value += 5; -/// } -/// ``` -#[derive(Debug)] -pub struct IterMut<'a, T: 'a> { - len: usize, - inner: iter::Enumerate<slice::IterMut<'a, Entry<T>>>, -} - -impl<'a, T> Iterator for IterMut<'a, T> { - type Item = (Index, &'a mut T); - - fn next(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next() { - Some((_, &mut Entry::Free { .. })) => continue, - Some(( - index, - &mut Entry::Occupied { - generation, - ref mut value, - }, - )) => { - self.len -= 1; - let idx = Index { index, generation }; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - (self.len, Some(self.len)) - } -} - -impl<'a, T> DoubleEndedIterator for IterMut<'a, T> { - fn next_back(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next_back() { - Some((_, &mut Entry::Free { .. })) => continue, - Some(( - index, - &mut Entry::Occupied { - generation, - ref mut value, - }, - )) => { - self.len -= 1; - let idx = Index { index, generation }; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } -} - -impl<'a, T> ExactSizeIterator for IterMut<'a, T> { - fn len(&self) -> usize { - self.len - } -} - -impl<'a, T> FusedIterator for IterMut<'a, T> {} - -/// An iterator that removes elements from the arena. -/// -/// Yields pairs of `(Index, T)` items. -/// -/// Order of iteration is not defined. -/// -/// Note: All elements are removed even if the iterator is only partially consumed or not consumed at all. -/// -/// # Examples -/// -/// ``` -/// use generational_arena::Arena; -/// -/// let mut arena = Arena::new(); -/// let idx_1 = arena.insert("hello"); -/// let idx_2 = arena.insert("world"); -/// -/// assert!(arena.get(idx_1).is_some()); -/// assert!(arena.get(idx_2).is_some()); -/// for (idx, value) in arena.drain() { -/// assert!((idx == idx_1 && value == "hello") || (idx == idx_2 && value == "world")); -/// } -/// assert!(arena.get(idx_1).is_none()); -/// assert!(arena.get(idx_2).is_none()); -/// ``` -#[derive(Debug)] -pub struct Drain<'a, T: 'a> { - len: usize, - inner: iter::Enumerate<vec::Drain<'a, Entry<T>>>, -} - -impl<'a, T> Iterator for Drain<'a, T> { - type Item = (Index, T); - - fn next(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next() { - Some((_, Entry::Free { .. })) => continue, - Some((index, Entry::Occupied { generation, value })) => { - let idx = Index { index, generation }; - self.len -= 1; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - (self.len, Some(self.len)) - } -} - -impl<'a, T> DoubleEndedIterator for Drain<'a, T> { - fn next_back(&mut self) -> Option<Self::Item> { - loop { - match self.inner.next_back() { - Some((_, Entry::Free { .. })) => continue, - Some((index, Entry::Occupied { generation, value })) => { - let idx = Index { index, generation }; - self.len -= 1; - return Some((idx, value)); - } - None => { - debug_assert_eq!(self.len, 0); - return None; - } - } - } - } -} - -impl<'a, T> ExactSizeIterator for Drain<'a, T> { - fn len(&self) -> usize { - self.len - } -} - -impl<'a, T> FusedIterator for Drain<'a, T> {} - -impl<T> Extend<T> for Arena<T> { - fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) { - for t in iter { - self.insert(t); - } - } -} - -impl<T> FromIterator<T> for Arena<T> { - fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { - let iter = iter.into_iter(); - let (lower, upper) = iter.size_hint(); - let cap = upper.unwrap_or(lower); - let cap = cmp::max(cap, 1); - let mut arena = Arena::with_capacity(cap); - arena.extend(iter); - arena - } -} - -impl<T> ops::Index<Index> for Arena<T> { - type Output = T; - - fn index(&self, index: Index) -> &Self::Output { - self.get(index).expect("No element at index") - } -} - -impl<T> ops::IndexMut<Index> for Arena<T> { - fn index_mut(&mut self, index: Index) -> &mut Self::Output { - self.get_mut(index).expect("No element at index") - } -} diff --git a/kayak_core/src/keys.rs b/kayak_core/src/keys.rs deleted file mode 100644 index 1510a65..0000000 --- a/kayak_core/src/keys.rs +++ /dev/null @@ -1,203 +0,0 @@ -/// The key code of a keyboard input. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[repr(u32)] -pub enum KeyCode { - /// The '1' key over the letters. - Key1, - /// The '2' key over the letters. - Key2, - /// The '3' key over the letters. - Key3, - /// The '4' key over the letters. - Key4, - /// The '5' key over the letters. - Key5, - /// The '6' key over the letters. - Key6, - /// The '7' key over the letters. - Key7, - /// The '8' key over the letters. - Key8, - /// The '9' key over the letters. - Key9, - /// The '0' key over the 'O' and 'P' keys. - Key0, - - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - - /// The Escape key, next to F1. - Escape, - - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - - /// Print Screen/SysRq. - Snapshot, - /// Scroll Lock. - Scroll, - /// Pause/Break key, next to Scroll lock. - Pause, - - /// `Insert`, next to Backspace. - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - - Left, - Up, - Right, - Down, - - /// The Backspace key, right over Enter. - Back, - /// The Enter key. - Return, - /// The space bar. - Space, - - /// The "Compose" key on Linux. - Compose, - - Caret, - - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - - AbntC1, - AbntC2, - NumpadAdd, - Apostrophe, - Apps, - Asterisk, - Plus, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - NumpadDecimal, - NumpadDivide, - Equals, - Grave, - Kana, - Kanji, - /// The left alt key. Maps to left option on Mac. - LAlt, - LBracket, - LControl, - LShift, - /// The left Windows key. Maps to left Command on Mac. - LWin, - Mail, - MediaSelect, - MediaStop, - Minus, - NumpadMultiply, - Mute, - MyComputer, - NavigateForward, // also called "Prior" - NavigateBackward, // also called "Next" - NextTrack, - NoConvert, - NumpadComma, - NumpadEnter, - NumpadEquals, - Oem102, - Period, - PlayPause, - Power, - PrevTrack, - /// The right alt key. Maps to right option on Mac. - RAlt, - RBracket, - RControl, - RShift, - /// The right Windows key. Maps to right Command on Mac. - RWin, - Semicolon, - Slash, - Sleep, - Stop, - NumpadSubtract, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, -} diff --git a/kayak_core/src/layout.rs b/kayak_core/src/layout.rs deleted file mode 100644 index 3d99271..0000000 --- a/kayak_core/src/layout.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::layout_cache::Rect; -use crate::Index; -pub use morphorm::GeometryChanged; - -/// A layout data sent to widgets on layout. -/// -/// Similar and interchangeable with [Rect] -/// ``` -/// use kayak_core::layout_cache::Rect; -/// use kayak_core::Layout; -/// -/// let layout = Layout::default(); -/// let rect : Rect = layout.into(); -/// let layout : Layout = rect.into(); -/// ``` -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub struct Layout { - /// width of the component - pub width: f32, - /// height of the component - pub height: f32, - /// x-coordinates of the component - pub x: f32, - /// y-coordinates of the component - pub y: f32, - /// z-coordinates of the component - pub z: f32, -} - -impl Layout { - /// Returns the position as a Kayak position type - pub fn pos(&self) -> (f32, f32) { - (self.x, self.y) - } -} - -impl From<Layout> for Rect { - fn from(layout: Layout) -> Self { - Rect { - posx: layout.x, - posy: layout.y, - width: layout.width, - height: layout.height, - z_index: layout.z, - } - } -} - -impl From<Rect> for Layout { - fn from(rect: Rect) -> Self { - Layout { - width: rect.width, - height: rect.height, - x: rect.posx, - y: rect.posy, - z: rect.z_index, - } - } -} - -/// -/// Struct used for [crate::OnLayout] as layout event data. -/// -pub struct LayoutEvent { - /// Layout of target component - pub layout: Layout, - /// Flags denoting the layout change. - /// - /// Note: The flags can potentially all be unset in cases where the [target's] layout did - /// not change, but one of its immediate children did. - /// - /// [target's]: LayoutEvent::target - pub flags: GeometryChanged, - /// The node ID of the element receiving the layout event. - pub target: Index, -} - -impl LayoutEvent { - pub(crate) fn new(rect: Rect, geometry_change: GeometryChanged, index: Index) -> LayoutEvent { - LayoutEvent { - layout: rect.into(), - flags: geometry_change, - target: index, - } - } -} diff --git a/kayak_core/src/layout_dispatcher.rs b/kayak_core/src/layout_dispatcher.rs deleted file mode 100644 index 2bd6815..0000000 --- a/kayak_core/src/layout_dispatcher.rs +++ /dev/null @@ -1,54 +0,0 @@ -use indexmap::IndexMap; -use morphorm::GeometryChanged; - -use crate::{Index, KayakContext, KayakContextRef, LayoutEvent}; - -pub(crate) struct LayoutEventDispatcher; - -impl LayoutEventDispatcher { - pub fn dispatch(context: &mut KayakContext) { - let changed = context - .widget_manager - .layout_cache - .iter_changed() - .map(|(index, flags)| (*index, *flags)) - .collect::<IndexMap<Index, GeometryChanged>>(); - - // Use IndexSet to prevent duplicates and maintain speed - let mut parents: IndexMap<Index, GeometryChanged> = IndexMap::default(); - - for (node_index, flags) in &changed { - // Add parent to set - if let Some(parent_index) = context.widget_manager.tree.get_parent(*node_index) { - if !changed.contains_key(&parent_index) { - parents.insert(parent_index, GeometryChanged::default()); - } - } - - // Process and dispatch - Self::process(*node_index, *flags, context); - } - - // Finally, process all parents - for (parent_index, flags) in parents { - // Process and dispatch - Self::process(parent_index, flags, context); - } - } - - fn process(index: Index, flags: GeometryChanged, context: &mut KayakContext) { - // We should be able to just get layout from WidgetManager here - // since the layouts will be calculated by this point - let widget = context.widget_manager.take(index); - if let Some(on_layout) = widget.get_props().get_on_layout() { - if let Some(rect) = context.widget_manager.layout_cache.rect.get(&index) { - let layout_event = LayoutEvent::new(*rect, flags, index); - let mut context_ref = KayakContextRef::new(context, Some(index)); - - on_layout.try_call(&mut context_ref, &layout_event); - } - } - - context.widget_manager.repossess(widget); - } -} diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs deleted file mode 100644 index 94a0b6e..0000000 --- a/kayak_core/src/lib.rs +++ /dev/null @@ -1,118 +0,0 @@ -mod assets; -mod binding; -mod children; -pub mod color; -pub mod context; -mod context_ref; -mod cursor; -mod cursor_icon; -pub mod event; -mod event_dispatcher; -mod flo_binding; -mod focus_tree; -pub mod fragment; -pub(crate) mod generational_arena; -mod input_event; -mod keyboard; -mod keys; -mod layout; -pub mod layout_cache; -mod layout_dispatcher; -mod lifetime; -mod multi_state; -pub mod node; -mod on_event; -mod on_layout; -pub mod render_command; -pub mod render_primitive; -pub mod styles; -pub mod tree; -mod vec; -pub mod widget; -pub mod widget_manager; - -use std::sync::{Arc, RwLock}; - -pub use binding::*; -pub use children::Children; -pub use color::Color; -pub use context::*; -pub use context_ref::KayakContextRef; -pub use cursor::*; -pub use cursor_icon::CursorIcon; -pub use event::*; -pub use focus_tree::FocusTree; -pub use fragment::{Fragment, FragmentProps}; -pub use generational_arena::{Arena, Index}; -pub use input_event::*; -pub use keyboard::{KeyboardEvent, KeyboardModifiers}; -pub use keys::KeyCode; -pub use layout::*; -pub use on_event::OnEvent; -pub use on_layout::OnLayout; -pub use resources::Resources; -pub use tree::{Tree, WidgetTree}; -pub use vec::{VecTracker, VecTrackerProps}; -pub use widget::{BaseWidget, Widget, WidgetProps}; - -/// The default font name used by Kayak -pub const DEFAULT_FONT: &str = "Kayak-Default"; - -/// Type alias for dynamic widget objects. We use [BaseWidget] so that we can be object-safe -type BoxedWidget = Box<dyn BaseWidget>; - -/// A simple handler object used for passing callbacks as props -/// -/// # Examples -/// -/// ``` -/// # use kayak_core::Handler; -/// -/// // Create a handler we can pass around -/// let on_select = Handler::new(|selected_index: usize| { -/// println!("Selected: {}", selected_index); -/// }); -/// -/// // Calling the handler can simply be done like -/// on_select.call(123); -/// ``` -#[derive(Clone)] -pub struct Handler<T = ()>(pub Arc<RwLock<dyn FnMut(T) + Send + Sync + 'static>>); - -impl<T> Default for Handler<T> { - fn default() -> Self { - Self(Arc::new(RwLock::new(|_| {}))) - } -} - -impl<T> Handler<T> { - /// Create a new handler callback - pub fn new<F: FnMut(T) + Send + Sync + 'static>(f: F) -> Handler<T> { - Handler(Arc::new(RwLock::new(f))) - } - - /// Call the handler - /// - /// # Panics - /// - /// Since the handler internally uses a `RwLock`, it can panic if the lock for the callback - /// is already held by the current thread. - pub fn call(&self, data: T) { - if let Ok(mut handler) = self.0.write() { - handler(data); - } - } -} - -/// Always returns true for handlers of the same generic type -impl<T> PartialEq for Handler<T> { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -impl<T> std::fmt::Debug for Handler<T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Handler").finish() - } -} diff --git a/kayak_core/src/lifetime.rs b/kayak_core/src/lifetime.rs deleted file mode 100644 index e3d41e8..0000000 --- a/kayak_core/src/lifetime.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::flo_binding::Uuid; -use crate::{Binding, Changeable, Releasable}; -use std::collections::HashMap; - -/// A container for storing callbacks tied to the lifetime of a widget -#[derive(Default)] -pub(crate) struct WidgetLifetime { - /// Maps a [`Binding`] by ID to a callback function - bindings: HashMap<Uuid, Box<dyn Releasable>>, -} - -impl WidgetLifetime { - /// Add a new callback for the given binding - /// - /// When the binding is changed via [`MutableBound`](crate::MutableBound), the given callback - /// will be invoked. This callback will exist for the entire life of the widget or until it - /// is removed via the [`remove`](Self::remove) or [`done`](Self::done) methods. - /// - /// # Arguments - /// - /// * `binding`: The binding to bind to - /// * `callback`: The callback to invoke when the binding changes - /// - pub fn add<TBinding, TCallback>(&mut self, binding: &Binding<TBinding>, callback: TCallback) - where - TBinding: resources::Resource + Clone + PartialEq, - TCallback: FnMut() -> () + Send + 'static, - { - let id = binding.id; - if self.bindings.contains_key(&id) { - // Binding already exists - return; - } - let releasable = binding.when_changed(crate::notify(callback)); - self.bindings.insert(id, releasable); - } - - /// Remove the callback for a given binding - /// - /// Returns the callback [`Releasable`] if it exists, otherwise `None`. - /// - /// # Arguments - /// - /// * `id`: The unique ID of the binding - /// - pub fn remove(&mut self, id: Uuid) -> Option<Box<dyn Releasable>> { - self.bindings.remove(&id) - } -} - -impl std::fmt::Debug for WidgetLifetime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WidgetLifetime") - .field("bindings", &self.bindings.keys()) - .finish() - } -} diff --git a/kayak_core/src/multi_state.rs b/kayak_core/src/multi_state.rs deleted file mode 100644 index e83bd5c..0000000 --- a/kayak_core/src/multi_state.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Handles storing more than one item in state.. -pub struct MultiState<T> { - pub data: Vec<T>, -} - -impl<T> MultiState<T> { - pub fn new(first_item: T) -> Self { - Self { - data: vec![first_item], - } - } - - pub fn get_or_add(&mut self, initial_value: T, index: &mut usize) -> &T { - if !self.data.get(*index).is_some() { - self.data.push(initial_value); - } - let item = &self.data[*index]; - *index += 1; - item - } - - pub fn get(&self, index: usize) -> &T { - let item = &self.data[index]; - item - } -} diff --git a/kayak_core/src/node.rs b/kayak_core/src/node.rs deleted file mode 100644 index c3ae3fd..0000000 --- a/kayak_core/src/node.rs +++ /dev/null @@ -1,465 +0,0 @@ -use crate::render_primitive::RenderPrimitive; -use crate::{ - styles::{Style, StyleProp}, - Arena, Index, -}; - -/// A widget node used for building the layout tree -#[derive(Debug, Clone, PartialEq)] -pub struct Node { - /// The list of children directly under this node - pub children: Vec<Index>, - /// The ID of this node's widget - pub id: Index, - /// The fully resolved styles for this node - pub resolved_styles: Style, - /// The raw styles for this node, before style resolution - pub raw_styles: Option<Style>, - /// The generated [`RenderPrimitive`] of this node - pub primitive: RenderPrimitive, - /// The z-index of this node, used for controlling layering - pub z: f32, -} - -/// A struct used for building a [`Node`] -pub struct NodeBuilder { - node: Node, -} - -impl NodeBuilder { - /// Defines a basic node without children, styles, etc. - pub fn empty() -> Self { - Self { - node: Node { - children: Vec::new(), - id: Index::default(), - resolved_styles: Style::default(), - raw_styles: None, - primitive: RenderPrimitive::Empty, - z: 0.0, - }, - } - } - - /// Defines a node with the given id and styles - pub fn new(id: Index, styles: Style) -> Self { - Self { - node: Node { - children: Vec::new(), - id, - resolved_styles: styles, - raw_styles: None, - primitive: RenderPrimitive::Empty, - z: 0.0, - }, - } - } - - /// Sets the ID of the node being built - pub fn with_id(mut self, id: Index) -> Self { - self.node.id = id; - self - } - - /// Sets the children of the node being built - pub fn with_children(mut self, children: Vec<Index>) -> Self { - self.node.children.extend(children); - self - } - - /// Sets the resolved and raw styles, respectively, of the node being built - pub fn with_styles(mut self, resolved_styles: Style, raw_styles: Option<Style>) -> Self { - self.node.resolved_styles = resolved_styles; - self.node.raw_styles = raw_styles; - self - } - - /// Sets the [`RenderPrimitive`] of the node being built - pub fn with_primitive(mut self, primitive: RenderPrimitive) -> Self { - self.node.primitive = primitive; - self - } - - /// Completes and builds the actual [`Node`] - pub fn build(self) -> Node { - self.node - } -} - -impl<'a> morphorm::Node<'a> for Index { - type Data = Arena<Option<Node>>; - - fn layout_type(&self, store: &'_ Self::Data) -> Option<morphorm::LayoutType> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.layout_type { - StyleProp::Default => Some(morphorm::LayoutType::default()), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::LayoutType::default()), - }; - } - } - return Some(morphorm::LayoutType::default()); - } - - fn position_type(&self, store: &'_ Self::Data) -> Option<morphorm::PositionType> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.position_type { - StyleProp::Default => Some(morphorm::PositionType::default()), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::PositionType::default()), - }; - } - } - return Some(morphorm::PositionType::default()); - } - - fn width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.width { - StyleProp::Default => Some(morphorm::Units::Stretch(1.0)), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Stretch(1.0)), - }; - } - } - return Some(morphorm::Units::Stretch(1.0)); - } - - fn height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.height { - StyleProp::Default => Some(morphorm::Units::Stretch(1.0)), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Stretch(1.0)), - }; - } - } - return Some(morphorm::Units::Stretch(1.0)); - } - - fn min_width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.min_width { - StyleProp::Default => Some(morphorm::Units::Pixels(0.0)), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn min_height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.min_height { - StyleProp::Default => Some(morphorm::Units::Pixels(0.0)), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn max_width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.max_width { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn max_height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.max_height { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.left { - StyleProp::Default => match node.resolved_styles.offset { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.left), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.right { - StyleProp::Default => match node.resolved_styles.offset { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.right), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.top { - StyleProp::Default => match node.resolved_styles.offset { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.top), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.bottom { - StyleProp::Default => match node.resolved_styles.offset { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.bottom), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn min_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn max_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn min_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn max_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn min_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn max_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn min_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn max_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { - Some(morphorm::Units::Auto) - } - - fn child_left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.padding_left { - StyleProp::Default => match node.resolved_styles.padding { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.left), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn child_right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.padding_right { - StyleProp::Default => match node.resolved_styles.padding { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.right), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn child_top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.padding_top { - StyleProp::Default => match node.resolved_styles.padding { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.top), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn child_bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.padding_bottom { - StyleProp::Default => match node.resolved_styles.padding { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop.bottom), - _ => Some(morphorm::Units::Auto), - }, - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - return Some(morphorm::Units::Auto); - } - - fn row_between(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.row_between { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn col_between(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.col_between { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(prop), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn grid_rows(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> { - Some(vec![]) - } - - fn grid_cols(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> { - Some(vec![]) - } - - fn row_index(&self, _store: &'_ Self::Data) -> Option<usize> { - Some(0) - } - - fn col_index(&self, _store: &'_ Self::Data) -> Option<usize> { - Some(0) - } - - fn row_span(&self, _store: &'_ Self::Data) -> Option<usize> { - Some(1) - } - - fn col_span(&self, _store: &'_ Self::Data) -> Option<usize> { - Some(1) - } - - fn border_left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.border { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.left)), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn border_right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.border { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.right)), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn border_top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.border { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.top)), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } - - fn border_bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { - if let Some(node) = store.get(*self) { - if let Some(node) = node { - return match node.resolved_styles.border { - StyleProp::Default => Some(morphorm::Units::Auto), - StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.bottom)), - _ => Some(morphorm::Units::Auto), - }; - } - } - Some(morphorm::Units::Auto) - } -} diff --git a/kayak_core/src/on_event.rs b/kayak_core/src/on_event.rs deleted file mode 100644 index a953554..0000000 --- a/kayak_core/src/on_event.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::{Event, KayakContextRef}; -use std::fmt::{Debug, Formatter}; -use std::sync::{Arc, RwLock}; - -/// A container for a function that handles events -/// -/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly -/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`] -/// as a parameter. -#[derive(Clone)] -pub struct OnEvent( - Arc<RwLock<dyn FnMut(&mut KayakContextRef, &mut Event) + Send + Sync + 'static>>, -); - -impl OnEvent { - /// Create a new event handler - /// - /// The handler should be a closure that takes the following arguments: - /// 1. The current context - /// 2. The event - pub fn new<F: FnMut(&mut KayakContextRef, &mut Event) + Send + Sync + 'static>( - f: F, - ) -> OnEvent { - OnEvent(Arc::new(RwLock::new(f))) - } - - /// Call the event handler - /// - /// Returns true if the handler was successfully invoked. - pub fn try_call(&self, context: &mut KayakContextRef, event: &mut Event) -> bool { - if let Ok(mut on_event) = self.0.write() { - on_event(context, event); - true - } else { - false - } - } -} - -impl Debug for OnEvent { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OnEvent").finish() - } -} - -impl PartialEq for OnEvent { - fn eq(&self, _: &Self) -> bool { - // Never prevent "==" for being true because of this struct - true - } -} diff --git a/kayak_core/src/on_layout.rs b/kayak_core/src/on_layout.rs deleted file mode 100644 index d0b3036..0000000 --- a/kayak_core/src/on_layout.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::layout::LayoutEvent; -use crate::KayakContextRef; -use std::fmt::{Debug, Formatter}; -use std::sync::{Arc, RwLock}; - -/// A container for a function that handles layout -/// -/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly -/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`] -/// as a parameter. -#[derive(Clone)] -pub struct OnLayout( - Arc<RwLock<dyn FnMut(&mut KayakContextRef, &LayoutEvent) + Send + Sync + 'static>>, -); - -impl OnLayout { - /// Create a new layout handler - /// - /// The handler should be a closure that takes the following arguments: - /// 1. The current context - /// 2. The LayoutEvent - pub fn new<F: FnMut(&mut KayakContextRef, &LayoutEvent) + Send + Sync + 'static>( - f: F, - ) -> OnLayout { - OnLayout(Arc::new(RwLock::new(f))) - } - - /// Call the layout handler - /// - /// Returns true if the handler was successfully invoked. - pub fn try_call(&self, context: &mut KayakContextRef, event: &LayoutEvent) -> bool { - if let Ok(mut on_layout) = self.0.write() { - on_layout(context, event); - true - } else { - false - } - } -} - -impl Debug for OnLayout { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OnLayout").finish() - } -} - -impl PartialEq for OnLayout { - fn eq(&self, _: &Self) -> bool { - // Never prevent "==" from being true because of this struct - true - } -} diff --git a/kayak_core/src/styles/option_ref.rs b/kayak_core/src/styles/option_ref.rs deleted file mode 100644 index bdf6f5a..0000000 --- a/kayak_core/src/styles/option_ref.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::styles::Style; - -/// A trait used to allow reading a value as an `Option<&T>` -pub trait AsRefOption<T> { - fn as_ref_option(&self) -> Option<&T>; -} - -impl AsRefOption<Style> for Style { - fn as_ref_option(&self) -> Option<&Style> { - Some(&self) - } -} - -impl AsRefOption<Style> for &Style { - fn as_ref_option(&self) -> Option<&Style> { - Some(self) - } -} - -impl AsRefOption<Style> for Option<Style> { - fn as_ref_option(&self) -> Option<&Style> { - self.as_ref() - } -} - -impl AsRefOption<Style> for &Option<Style> { - fn as_ref_option(&self) -> Option<&Style> { - self.as_ref() - } -} diff --git a/kayak_core/src/vec.rs b/kayak_core/src/vec.rs deleted file mode 100644 index b70767c..0000000 --- a/kayak_core/src/vec.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::{ - context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, OnLayout, Widget, - WidgetProps, -}; - -/// Props used by the [`VecTracker`] widget -#[derive(Default, Debug, PartialEq, Clone)] -pub struct VecTrackerProps<T> { - /// The data to display in sequence - /// - /// The type of [T] should be implement the [`Widget`] trait - pub data: Vec<T>, - pub styles: Option<Style>, - pub children: Option<Children>, - pub on_event: Option<OnEvent>, - pub on_layout: Option<OnLayout>, -} - -/// A widget that renders a `Vec` of widgets -/// -/// # Props -/// -/// __Type:__ [`VecTrackerProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `focusable` | ⌠| -/// -#[derive(Debug, PartialEq, Clone, Default)] -pub struct VecTracker<T> { - pub id: Index, - pub props: VecTrackerProps<T>, -} - -impl<T> VecTracker<T> { - pub fn new(data: Vec<T>) -> Self { - let props = VecTrackerProps { - data, - styles: None, - children: None, - on_event: None, - on_layout: None, - }; - - Self { - id: Index::default(), - props, - } - } -} - -impl<T, I> From<I> for VecTracker<T> -where - I: Iterator<Item = T>, -{ - fn from(iter: I) -> Self { - Self::new(iter.collect()) - } -} - -impl<T> WidgetProps for VecTrackerProps<T> -where - T: Widget, -{ - fn get_children(&self) -> Option<Children> { - self.children.clone() - } - - fn set_children(&mut self, children: Option<Children>) { - self.children = children; - } - - fn get_styles(&self) -> Option<Style> { - self.styles.clone() - } - - fn get_on_event(&self) -> Option<OnEvent> { - self.on_event.clone() - } - - fn get_on_layout(&self) -> Option<OnLayout> { - self.on_layout.clone() - } - - fn get_focusable(&self) -> Option<bool> { - Some(false) - } -} - -impl<T> Widget for VecTracker<T> -where - T: Widget, -{ - type Props = VecTrackerProps<T>; - - fn constructor(props: Self::Props) -> Self - where - Self: Sized, - { - Self { - id: Index::default(), - props, - } - } - - fn get_id(&self) -> Index { - self.id - } - - fn set_id(&mut self, id: Index) { - self.id = id; - } - - fn get_props(&self) -> &Self::Props { - &self.props - } - - fn get_props_mut(&mut self) -> &mut Self::Props { - &mut self.props - } - - fn render(&mut self, context: &mut KayakContextRef) { - for (index, item) in self.props.data.iter().enumerate() { - context.add_widget(item.clone(), index); - } - - context.commit(); - } -} diff --git a/kayak_core/src/widget.rs b/kayak_core/src/widget.rs deleted file mode 100644 index ba896e4..0000000 --- a/kayak_core/src/widget.rs +++ /dev/null @@ -1,179 +0,0 @@ -use as_any::AsAny; -use std::any::Any; - -use crate::on_layout::OnLayout; -use crate::{context_ref::KayakContextRef, styles::Style, Children, Event, Index, OnEvent}; - -/// An internal trait that has a blanket implementation over all implementors of [`Widget`] -/// -/// This ensures that [`BaseWidget`] can never be implemented manually outside of this crate, even -/// if it is exported out (as long as this one isn't). -pub trait SealedWidget {} - -/// The base widget trait, used internally -/// -/// You should _never_ implement BaseWidget manually. It is automatically implemented on -/// all implementors of [`Widget`]. -pub trait BaseWidget: SealedWidget + std::fmt::Debug + Send + Sync { - fn constructor<P: WidgetProps>(props: P) -> Self - where - Self: Sized; - fn get_id(&self) -> Index; - fn set_id(&mut self, id: Index); - fn get_props(&self) -> &dyn WidgetProps; - fn get_props_mut(&mut self) -> &mut dyn WidgetProps; - fn render(&mut self, context: &mut KayakContextRef); - fn get_name(&self) -> &'static str; - fn on_event(&mut self, context: &mut KayakContextRef, event: &mut Event); -} - -/// The main trait for defining a widget -pub trait Widget: std::fmt::Debug + Clone + Default + PartialEq + AsAny + Send + Sync { - /// The props associated with this widget - type Props: WidgetProps + Clone + Default + PartialEq; - - /// Construct the widget with the given props - fn constructor(props: Self::Props) -> Self - where - Self: Sized; - - /// Get this widget's ID - fn get_id(&self) -> Index; - - /// Set this widget's ID - /// - /// This method is used internally. You likely do not (or should not) need to call this yourself. - fn set_id(&mut self, id: Index); - - /// Get a reference to this widget's props - fn get_props(&self) -> &Self::Props; - - /// Get a mutable reference to this widget's props - fn get_props_mut(&mut self) -> &mut Self::Props; - - /// The render function for this widget - /// - /// This method will be called whenever the widget needs to be re-rendered. It should build its - /// own [`WidgetTree`](crate::WidgetTree) using [`KayakContextRef`](crate::KayakContextRef) and finalize - /// the tree using [`KayakContextRef::commit`](crate::KayakContextRef::commit). - fn render(&mut self, context: &mut KayakContextRef); - - /// Get the name of this widget - fn get_name(&self) -> &'static str { - std::any::type_name::<Self>() - } - - /// Send an event to this widget - fn on_event(&mut self, context: &mut KayakContextRef, event: &mut Event) { - if let Some(on_event) = self.get_props().get_on_event() { - on_event.try_call(context, event); - } - } -} - -/// Trait for props passed to a widget -pub trait WidgetProps: std::fmt::Debug + AsAny + Send + Sync { - /// Gets the children of this widget - /// - /// Returns `None` if this widget doesn't contain children - fn get_children(&self) -> Option<Children>; - /// Sets the children of this widget - fn set_children(&mut self, children: Option<Children>); - /// Gets the custom styles of this widget - /// - /// Returns `None` if this widget doesn't have any custom styles - fn get_styles(&self) -> Option<Style>; - /// Gets the custom event handler of this widget - /// - /// Returns `None` if this widget doesn't contain a custom event handler - fn get_on_event(&self) -> Option<OnEvent>; - /// Gets the custom layout event handler of this widget - /// - /// Returns `None` if this widget doesn't contain a custom layout event handler - fn get_on_layout(&self) -> Option<OnLayout>; - /// Gets the focusability of this widget - /// - /// The meanings of the returned values are: - /// - /// | Value | Description | - /// |---------------|------------------------------------------| - /// | `Some(true)` | The widget is focusable | - /// | `Some(false)` | The widget is not focusable | - /// | `None` | The widget's focusability is unspecified | - /// - fn get_focusable(&self) -> Option<bool>; -} - -/// Automatically implements the `BaseWidget` trait for all implementors of [`Widget`] -impl<T> BaseWidget for T -where - T: Widget + Clone + PartialEq + Default, -{ - fn constructor<P: WidgetProps>(props: P) -> Self - where - Self: Sized, - { - let props: Box<dyn Any> = Box::new(props); - Widget::constructor(*props.downcast::<<T as Widget>::Props>().unwrap()) - } - - fn get_id(&self) -> Index { - Widget::get_id(self) - } - - fn set_id(&mut self, id: Index) { - Widget::set_id(self, id); - } - - fn get_props(&self) -> &dyn WidgetProps { - Widget::get_props(self) - } - - fn get_props_mut(&mut self) -> &mut dyn WidgetProps { - Widget::get_props_mut(self) - } - - fn render(&mut self, context: &mut KayakContextRef) { - Widget::render(self, context); - } - - fn get_name(&self) -> &'static str { - Widget::get_name(self) - } - - fn on_event(&mut self, context: &mut KayakContextRef, event: &mut Event) { - Widget::on_event(self, context, event); - } -} - -/// Automatically implements the `SealedWidget` trait for all implementors of [`Widget`] -impl<T> SealedWidget for T where T: Widget {} - -/// Implements [`WidgetProps`] for the unit type, allowing for "empty" props to -/// be defined as a simple `()` -/// -/// This just reduces the amount of code and imports needed to state that a widget contains -/// no adjustable props (i.e., it's top-level or fully contained). -impl WidgetProps for () { - fn get_children(&self) -> Option<Children> { - None - } - - fn set_children(&mut self, _children: Option<Children>) {} - - fn get_styles(&self) -> Option<Style> { - None - } - - fn get_on_event(&self) -> Option<OnEvent> { - None - } - - fn get_on_layout(&self) -> Option<OnLayout> { - None - } - - fn get_focusable(&self) -> Option<bool> { - None - } -} diff --git a/kayak_core/src/widget_manager.rs b/kayak_core/src/widget_manager.rs deleted file mode 100644 index 6bd30ed..0000000 --- a/kayak_core/src/widget_manager.rs +++ /dev/null @@ -1,550 +0,0 @@ -use indexmap::IndexSet; -use kayak_font::KayakFont; -use morphorm::Units; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -use crate::assets::Assets; -use crate::layout_cache::Rect; -use crate::lifetime::WidgetLifetime; -use crate::styles::StyleProp; -use crate::{ - focus_tree::FocusTracker, - focus_tree::FocusTree, - layout_cache::LayoutCache, - node::{Node, NodeBuilder}, - render_command::RenderCommand, - render_primitive::RenderPrimitive, - styles::Style, - tree::Tree, - Arena, Binding, Bound, BoxedWidget, Index, Widget, WidgetProps, -}; -// use as_any::Downcast; - -#[derive(Debug)] -pub struct WidgetManager { - pub(crate) current_widgets: Arena<Option<BoxedWidget>>, - pub(crate) dirty_render_nodes: IndexSet<Index>, - pub(crate) dirty_nodes: Arc<Mutex<IndexSet<Index>>>, - pub(crate) nodes: Arena<Option<Node>>, - /// A mapping of widgets to their lifetime - widget_lifetimes: HashMap<Index, WidgetLifetime>, - /// A tree containing all widgets in the hierarchy. - pub tree: Tree, - /// A tree containing only the widgets with layouts in the hierarchy. - pub node_tree: Tree, - /// A tree containing all actively focusable widgets. - pub focus_tree: FocusTree, - pub layout_cache: LayoutCache, - focus_tracker: FocusTracker, - current_z: f32, -} - -impl WidgetManager { - pub fn new() -> Self { - Self { - current_widgets: Arena::new(), - dirty_render_nodes: IndexSet::new(), - dirty_nodes: Arc::new(Mutex::new(IndexSet::new())), - nodes: Arena::new(), - tree: Tree::default(), - node_tree: Tree::default(), - layout_cache: LayoutCache::default(), - focus_tree: FocusTree::default(), - focus_tracker: FocusTracker::default(), - current_z: 0.0, - widget_lifetimes: HashMap::new(), - } - } - - /// Re-renders from the root. - /// If force is true sets ALL nodes to re-render. - /// Can be slow. - pub fn dirty(&mut self, force: bool) { - // Force tree to re-render from root. - if let Ok(mut dirty_nodes) = self.dirty_nodes.lock() { - dirty_nodes.insert(self.tree.root_node.unwrap()); - - if force { - for (node_index, _) in self.current_widgets.iter() { - dirty_nodes.insert(node_index); - self.dirty_render_nodes.insert(node_index); - } - } - } - } - - pub fn create_widget<T: Widget + 'static>( - &mut self, - index: usize, - mut widget: T, - parent: Option<Index>, - ) -> (bool, Index) { - let widget_id = if let Some(parent) = parent.clone() { - if let Some(parent_children) = self.tree.children.get_mut(&parent) { - parent_children.get(index).cloned() - } else { - None - } - } else { - None - }; - - // Pull child and update. - if let Some(widget_id) = widget_id { - widget.set_id(widget_id); - // Remove from the dirty nodes lists. - // if let Some(index) = self.dirty_nodes.iter().position(|id| *widget_id == *id) { - // self.dirty_nodes.remove(index); - // } - - // Mark this widget as focusable if it's designated focusable or if it's the root node - if self.tree.is_empty() { - self.set_focusable(Some(true), widget_id, true); - } else { - self.set_focusable(widget.get_props().get_focusable(), widget_id, true); - } - - // TODO: Figure a good way of diffing props passed to children of a widget - // that wont naturally-rerender it's children because of a lack of changes - // to it's own props. - // if &widget - // != self.current_widgets[*widget_id] - // .as_ref() - // .unwrap() - // .downcast_ref::<T>() - // .unwrap() - // { - let boxed_widget: BoxedWidget = Box::new(widget); - *self.current_widgets[widget_id].as_mut().unwrap() = boxed_widget; - // Tell renderer that the nodes changed. - self.dirty_render_nodes.insert(widget_id); - return (true, widget_id); - // } else { - // return (false, *widget_id); - // } - } - - // Mark this widget as focusable if it's designated focusable or if it's the root node - let focusable = if self.tree.is_empty() { - Some(true) - } else { - widget.get_props().get_focusable() - }; - - // Create Flow - // We should only have one widget that doesn't have a parent. - // The root widget. - let widget_id = self.current_widgets.insert(Some(Box::new(widget))); - self.nodes.insert(None); - self.current_widgets[widget_id] - .as_mut() - .unwrap() - .set_id(widget_id); - - // Tell renderer that the nodes changed. - self.dirty_render_nodes.insert(widget_id); - - // Remove from the dirty nodes lists. - // if let Some(index) = self.dirty_nodes.iter().position(|id| widget_id == *id) { - // self.dirty_nodes.remove(index); - // } - - self.tree.add(widget_id, parent); - self.layout_cache.add(widget_id); - self.set_focusable(focusable, widget_id, true); - - (true, widget_id) - } - - pub fn take(&mut self, id: Index) -> BoxedWidget { - self.current_widgets[id].take().unwrap() - } - - pub fn repossess(&mut self, widget: BoxedWidget) { - let widget_id = widget.get_id(); - self.current_widgets[widget_id] = Some(widget); - } - - pub fn get_layout(&self, id: &Index) -> Option<&Rect> { - self.layout_cache.rect.get(id) - } - - pub fn get_name(&self, id: &Index) -> Option<String> { - if let Some(widget) = &self.current_widgets[*id] { - return Some(widget.get_name().to_string()); - } - - None - } - - /// Render the widget tree. - /// - /// This will call [`calculate_layout`] automatically and recurse if any widget - /// has unresolved layout dependencies (up to a maximum recursion depth of 2). - /// - /// [`calculate_layout`]: Self::calculate_layout - pub fn render(&mut self, assets: &mut Assets) { - self.render_internal(assets, 0); - } - - fn render_internal(&mut self, assets: &mut Assets, depth: usize) { - // This is the maximum recursion depth for this method. - // Recursion involves recalculating layout which should be done sparingly. - const MAX_RECURSION_DEPTH: usize = 2; - - let initial_styles = Style::initial(); - let default_styles = Style::new_default(); - let nodes: Vec<_> = self.dirty_render_nodes.drain(..).collect(); - for dirty_node_index in nodes { - let dirty_widget = self.current_widgets[dirty_node_index].as_ref().unwrap(); - // Get the parent styles. Will be one of the following: - // 1. Already-resolved node styles (best) - // 2. Unresolved widget prop styles - // 3. Unresolved default styles - let parent_styles = - if let Some(parent_widget_id) = self.tree.parents.get(&dirty_node_index) { - if let Some(parent) = self.nodes[*parent_widget_id].as_ref() { - parent.resolved_styles.clone() - } else if let Some(parent) = self.current_widgets[*parent_widget_id].as_ref() { - if let Some(styles) = parent.get_props().get_styles() { - styles - } else { - default_styles.clone() - } - } else { - default_styles.clone() - } - } else { - default_styles.clone() - }; - - // Get parent Z - let parent_z = if let Some(parent_widget_id) = self.tree.parents.get(&dirty_node_index) - { - if let Some(parent) = &self.nodes[*parent_widget_id] { - parent.z - } else { - -1.0 - } - } else { - -1.0 - }; - - let current_z = { - if parent_z > -1.0 { - parent_z + 1.0 - } else { - let z = self.current_z; - self.current_z += 1.0; - z - } - }; - - let raw_styles = dirty_widget.get_props().get_styles(); - let mut styles = raw_styles.clone().unwrap_or_default(); - // Fill in all `initial` values for any unset property - styles.apply(&initial_styles); - // Fill in all `inherited` values for any `inherit` property - styles.inherit(&parent_styles); - - let primitive = self.create_primitive(dirty_node_index, &mut styles, assets); - - let children = self - .tree - .children - .get(&dirty_node_index) - .cloned() - .unwrap_or(vec![]); - - let mut node = NodeBuilder::empty() - .with_id(dirty_node_index) - .with_styles(styles, raw_styles) - .with_children(children) - .with_primitive(primitive) - .build(); - node.z = current_z; - - self.nodes[dirty_node_index] = Some(node); - } - - self.node_tree = self.build_nodes_tree(); - self.calculate_layout(); - - if !self.dirty_render_nodes.is_empty() && depth < MAX_RECURSION_DEPTH { - // If not empty, then there are nodes that need layout to be re-calculated - // before they can properly render. - self.render_internal(assets, depth + 1); - } - } - - pub fn calculate_layout(&mut self) { - morphorm::layout(&mut self.layout_cache, &self.node_tree, &self.nodes); - } - - fn create_primitive( - &mut self, - id: Index, - styles: &mut Style, - assets: &mut Assets, - ) -> RenderPrimitive { - let mut render_primitive = RenderPrimitive::from(&styles.clone()); - let mut needs_layout = false; - - match &mut render_primitive { - RenderPrimitive::Text { - content, - font, - properties, - text_layout, - .. - } => { - // --- Bind to Font Asset --- // - let asset = assets.get_asset::<KayakFont, _>(font.clone()); - self.bind(id, &asset); - - if let Some(font) = asset.get() { - if let Some(parent_id) = self.get_valid_parent(id) { - if let Some(parent_layout) = self.get_layout(&parent_id) { - properties.max_size = (parent_layout.width, parent_layout.height); - - // --- Calculate Text Layout --- // - *text_layout = font.measure(&content, *properties); - let measurement = text_layout.size(); - - // --- Apply Layout --- // - if matches!(styles.width, StyleProp::Default) { - styles.width = StyleProp::Value(Units::Pixels(measurement.0)); - } - if matches!(styles.height, StyleProp::Default) { - styles.height = StyleProp::Value(Units::Pixels(measurement.1)); - } - } else { - needs_layout = true; - } - } - } - } - _ => {} - } - - if needs_layout { - self.needs_layout(id); - } - - render_primitive - } - - fn recurse_node_tree_to_build_primitives( - node_tree: &Tree, - layout_cache: &LayoutCache, - nodes: &Arena<Option<Node>>, - current_node: Index, - mut main_z_index: f32, - mut prev_clip: RenderPrimitive, - ) -> Vec<RenderPrimitive> { - let mut render_primitives = Vec::new(); - - if let Some(node) = nodes.get(current_node).unwrap() { - if let Some(layout) = layout_cache.rect.get(¤t_node) { - let mut render_primitive = node.primitive.clone(); - let mut layout = *layout; - let new_z_index = if matches!(render_primitive, RenderPrimitive::Clip { .. }) { - main_z_index - 0.1 - } else { - main_z_index - }; - layout.z_index = new_z_index; - render_primitive.set_layout(layout); - render_primitives.push(render_primitive.clone()); - - let new_prev_clip = if matches!(render_primitive, RenderPrimitive::Clip { .. }) { - render_primitive.clone() - } else { - prev_clip - }; - - prev_clip = new_prev_clip.clone(); - - if node_tree.children.contains_key(¤t_node) { - for child in node_tree.children.get(¤t_node).unwrap() { - main_z_index += 1.0; - render_primitives.extend(Self::recurse_node_tree_to_build_primitives( - node_tree, - layout_cache, - nodes, - *child, - main_z_index, - new_prev_clip.clone(), - )); - - main_z_index = layout.z_index; - // Between each child node we need to reset the clip. - if matches!(prev_clip, RenderPrimitive::Clip { .. }) { - // main_z_index = new_z_index; - match &mut prev_clip { - RenderPrimitive::Clip { layout } => { - layout.z_index = main_z_index + 0.1; - } - _ => {} - }; - render_primitives.push(prev_clip.clone()); - } - } - } - } - } - - render_primitives - } - - /// Forces layout to be recalculated before rendering. - /// - /// This should be used _sparingly_, if at all. - pub fn needs_layout(&mut self, id: Index) { - self.dirty_render_nodes.insert(id); - } - - pub fn build_render_primitives(&self) -> Vec<RenderPrimitive> { - if self.node_tree.root_node.is_none() { - return vec![]; - } - - Self::recurse_node_tree_to_build_primitives( - &self.node_tree, - &self.layout_cache, - &self.nodes, - self.node_tree.root_node.unwrap(), - 0.0, - RenderPrimitive::Empty, - ) - } - - fn build_nodes_tree(&mut self) -> Tree { - let mut tree = Tree::default(); - let (root_node_id, _) = self.current_widgets.iter().next().unwrap(); - tree.root_node = Some(root_node_id); - tree.children.insert( - tree.root_node.unwrap(), - self.get_valid_node_children(tree.root_node.unwrap()), - ); - - let old_focus = self.focus_tree.current(); - self.focus_tree.clear(); - self.focus_tree.add(root_node_id, &self.tree); - - for (widget_id, widget) in self.current_widgets.iter().skip(1) { - let widget_styles = widget.as_ref().unwrap().get_props().get_styles(); - if let Some(widget_styles) = widget_styles { - // Only add widgets who have renderable nodes. - if widget_styles.render_command.resolve() != RenderCommand::Empty { - let valid_children = self.get_valid_node_children(widget_id); - tree.children.insert(widget_id, valid_children); - let valid_parent = self.get_valid_parent(widget_id); - if let Some(valid_parent) = valid_parent { - tree.parents.insert(widget_id, valid_parent); - } - } - } - - let focusable = self.get_focusable(widget_id).unwrap_or_default(); - if focusable { - self.focus_tree.add(widget_id, &self.tree); - } - } - - if let Some(old_focus) = old_focus { - if self.focus_tree.contains(old_focus) { - self.focus_tree.focus(old_focus); - } - } - - tree - } - - pub fn get_valid_node_children(&self, node_id: Index) -> Vec<Index> { - let mut children = Vec::new(); - if let Some(node_children) = self.tree.children.get(&node_id) { - for child_id in node_children { - if let Some(child_widget) = &self.current_widgets[*child_id] { - if let Some(child_styles) = child_widget.get_props().get_styles() { - if child_styles.render_command.resolve() != RenderCommand::Empty { - children.push(*child_id); - } else { - children.extend(self.get_valid_node_children(*child_id)); - } - } else { - children.extend(self.get_valid_node_children(*child_id)); - } - } - } - } - - children - } - - pub fn get_valid_parent(&self, node_id: Index) -> Option<Index> { - if let Some(parent_id) = self.tree.parents.get(&node_id) { - if let Some(parent_widget) = &self.nodes[*parent_id] { - if parent_widget.resolved_styles.render_command.resolve() != RenderCommand::Empty { - return Some(*parent_id); - } - } - return self.get_valid_parent(*parent_id); - } - // assert!(node_id.into_raw_parts().0 == 0); - None - } - - pub fn get_node(&self, id: &Index) -> Option<Node> { - self.nodes[*id].clone() - } - - /// Bind a widget so that it re-renders when the binding changes - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// * `binding`: the binding to watch - /// - pub(crate) fn bind<T>(&mut self, id: Index, binding: &Binding<T>) - where - T: resources::Resource + Clone + PartialEq, - { - let dirty_nodes = self.dirty_nodes.clone(); - let lifetime = self.widget_lifetimes.entry(id).or_default(); - lifetime.add(binding, move || { - if let Ok(mut dirty_nodes) = dirty_nodes.lock() { - dirty_nodes.insert(id); - } - }); - } - - /// Unbinds a binding from a widget - /// - /// Returns true if the binding was successfully removed, or false if the binding - /// does not exist on the given widget. - /// - /// # Arguments - /// - /// * `id`: The ID of the widget - /// * `binding_id`: The ID of the binding - /// - #[allow(dead_code)] - pub(crate) fn unbind(&mut self, id: Index, binding_id: crate::flo_binding::Uuid) -> bool { - if let Some(lifetime) = self.widget_lifetimes.get_mut(&id) { - lifetime.remove(binding_id).is_some() - } else { - false - } - } - - pub fn get_focusable(&self, index: Index) -> Option<bool> { - self.focus_tracker.get_focusability(index) - } - - pub fn set_focusable(&mut self, focusable: Option<bool>, index: Index, is_parent: bool) { - self.focus_tracker - .set_focusability(index, focusable, is_parent); - } -} diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml index 428924f..40ce386 100644 --- a/kayak_font/Cargo.toml +++ b/kayak_font/Cargo.toml @@ -15,16 +15,8 @@ unicode-segmentation = "1.9" # Provides UAX #14 line break segmentation xi-unicode = "0.3" -[dependencies.bevy] -version = "0.8.0" -optional = true -default-features = false -features = [ - "bevy_asset", - "bevy_render", - "bevy_core_pipeline" -] +bevy = { git = "https://github.com/bevyengine/bevy", rev="9423cb6a8d0c140e11364eb23c8feb7e576baa8c", optional = true, default-features = false, features = ["bevy_asset", "bevy_render", "bevy_core_pipeline"] } [dev-dependencies] -bevy = "0.8" +bevy = { git = "https://github.com/bevyengine/bevy", rev="9423cb6a8d0c140e11364eb23c8feb7e576baa8c" } bytemuck = "1.12.0" diff --git a/kayak_font/assets/roboto.png b/kayak_font/assets/roboto.png deleted file mode 100644 index 1e35a6327fe8243d1e51ca56b06183733799628d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98252 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%0Dnd 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 diff --git a/kayak_font/examples/bevy.rs b/kayak_font/examples/bevy.rs index d509261..7284566 100644 --- a/kayak_font/examples/bevy.rs +++ b/kayak_font/examples/bevy.rs @@ -24,13 +24,12 @@ const INSTRUCTIONS: &str = struct Instructions; fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { - commands.spawn_bundle(Camera2dBundle::default()); + commands.spawn(Camera2dBundle::default()); let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font"); commands - .spawn() - .insert(Text { + .spawn(Text { horz_alignment: Alignment::Start, color: Color::WHITE, content: "Hello World! This text should wrap because it's kinda-super-long. How cool is that?!\nHere's a new line.\n\tHere's a tab.".into(), @@ -41,7 +40,7 @@ fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { }) .insert(font_handle.clone()); - commands.spawn().insert_bundle(SpriteBundle { + commands.spawn(SpriteBundle { sprite: Sprite { color: Color::DARK_GRAY, custom_size: Some(INITIAL_SIZE), @@ -55,9 +54,8 @@ fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { ..Default::default() }); - commands - .spawn() - .insert(Text { + commands.spawn(( + Text { horz_alignment: Alignment::Middle, color: Color::WHITE, content: INSTRUCTIONS.into(), @@ -65,9 +63,10 @@ fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { line_height: 32.0 * 1.2, // Firefox method of calculating default line heights see: https://developer.mozilla.org/en-US/docs/Web/CSS/line-height position: Vec2::new(-360.0, 250.0), size: Vec2::new(720.0, 200.0), - }) - .insert(Instructions) - .insert(font_handle.clone()); + }, + Instructions, + font_handle.clone(), + )); } fn control_text( diff --git a/kayak_font/examples/renderer/extract.rs b/kayak_font/examples/renderer/extract.rs index 923923c..8c1667c 100644 --- a/kayak_font/examples/renderer/extract.rs +++ b/kayak_font/examples/renderer/extract.rs @@ -1,8 +1,7 @@ use bevy::{ math::Vec2, - prelude::{Assets, Commands, Handle, Query, Res}, + prelude::{Assets, Commands, Handle, Query, Rect, Res}, render::Extract, - sprite::Rect, }; use kayak_font::{KayakFont, TextProperties}; diff --git a/kayak_font/examples/renderer/pipeline.rs b/kayak_font/examples/renderer/pipeline.rs index 32f5efd..f2738c4 100644 --- a/kayak_font/examples/renderer/pipeline.rs +++ b/kayak_font/examples/renderer/pipeline.rs @@ -5,7 +5,9 @@ use bevy::{ SystemState, }, math::{Mat4, Quat, Vec2, Vec3, Vec4}, - prelude::{Bundle, Component, Entity, FromWorld, Handle, Query, Res, ResMut, World}, + prelude::{ + Bundle, Component, Entity, FromWorld, Handle, Query, Rect, Res, ResMut, Resource, World, + }, render::{ color::Color, render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass}, @@ -23,7 +25,6 @@ use bevy::{ texture::{BevyDefault, GpuImage}, view::{ViewUniformOffset, ViewUniforms}, }, - sprite::Rect, utils::FloatOrd, }; use bytemuck::{Pod, Zeroable}; @@ -34,6 +35,7 @@ use kayak_font::{ use super::FONT_SHADER_HANDLE; +#[derive(Resource)] pub struct FontPipeline { view_layout: BindGroupLayout, pub(crate) font_image_layout: BindGroupLayout, @@ -215,6 +217,7 @@ struct QuadType { pub t: i32, } +#[derive(Resource)] pub struct QuadMeta { vertices: BufferVec<QuadVertex>, view_bind_group: Option<BindGroup>, diff --git a/kayak_font/src/bevy/renderer/extract.rs b/kayak_font/src/bevy/renderer/extract.rs index 490ba4c..94bc95c 100644 --- a/kayak_font/src/bevy/renderer/extract.rs +++ b/kayak_font/src/bevy/renderer/extract.rs @@ -1,13 +1,15 @@ use crate::bevy::renderer::FontTextureCache; use crate::KayakFont; -use bevy::prelude::{AssetEvent, Assets, Commands, EventReader, Handle, Image, Local, Res, ResMut}; +use bevy::prelude::{ + AssetEvent, Assets, Commands, EventReader, Handle, Image, Local, Res, ResMut, Resource, +}; use bevy::render::{ render_resource::{TextureFormat, TextureUsages}, Extract, }; use bevy::utils::HashSet; -#[derive(Default)] +#[derive(Default, Resource)] pub struct ExtractedFonts { pub fonts: Vec<(Handle<KayakFont>, KayakFont)>, } diff --git a/kayak_font/src/bevy/renderer/font_texture_cache.rs b/kayak_font/src/bevy/renderer/font_texture_cache.rs index 8f4acdc..3213ccd 100644 --- a/kayak_font/src/bevy/renderer/font_texture_cache.rs +++ b/kayak_font/src/bevy/renderer/font_texture_cache.rs @@ -1,7 +1,7 @@ use crate::{KayakFont, Sdf}; use bevy::{ math::Vec2, - prelude::{Handle, Res}, + prelude::{Handle, Res, Resource}, render::{ render_asset::RenderAssets, render_resource::{ @@ -22,6 +22,7 @@ pub trait FontRenderingPipeline { pub const MAX_CHARACTERS: u32 = 500; +#[derive(Resource)] pub struct FontTextureCache { images: HashMap<Handle<KayakFont>, GpuImage>, pub(crate) bind_groups: HashMap<Handle<KayakFont>, BindGroup>, diff --git a/kayak_render_macros/examples/main.rs b/kayak_render_macros/examples/main.rs deleted file mode 100644 index d93cfae..0000000 --- a/kayak_render_macros/examples/main.rs +++ /dev/null @@ -1,63 +0,0 @@ -use kayak_core::{context::KayakContext, styles::Style, Children}; -use kayak_core::{Fragment, KayakContextRef}; -use kayak_render_macros::{rsx, use_state, widget, WidgetProps}; - -#[derive(WidgetProps, Clone, Default, Debug, PartialEq)] -#[allow(dead_code)] -struct TestProps { - /// A test prop - foo: u32, - #[prop_field(Styles)] - styles: Option<Style>, - #[prop_field(Children)] - children: Option<Children>, - #[prop_field(OnEvent)] - on_event: Option<kayak_core::OnEvent>, -} - -#[widget] -/// A test widget -fn Test(props: TestProps) { - let _ = use_state!(props.foo); - let children = props.get_children(); - rsx! { - <>{children}</> - }; -} - -fn main() { - let mut context = KayakContext::new(); - { - let mut context = KayakContextRef::new(&mut context, None); - let foo = 10; - let test_styles = Style::default(); - let children: Option<kayak_core::Children> = None; - rsx! { - <Fragment> - <Test foo={10}> - <Test foo={1}> - <Test foo={5}> - <Test foo={foo} styles={Some(test_styles)}> - {} - </Test> - </Test> - </Test> - </Test> - </Fragment> - }; - - let foo = 10; - let test_styles = Style::default(); - let children: Option<kayak_core::Children> = None; - rsx! { - <Fragment> - <Test foo={foo} styles={Some(test_styles)}> - {} - </Test> - <Test foo={5} styles={Some(test_styles)}> - {} - </Test> - </Fragment> - } - } -} diff --git a/kayak_render_macros/src/child.rs b/kayak_render_macros/src/child.rs deleted file mode 100644 index f832af6..0000000 --- a/kayak_render_macros/src/child.rs +++ /dev/null @@ -1,58 +0,0 @@ -use quote::{quote, ToTokens}; -use syn::parse::{Parse, ParseStream, Result}; - -use crate::widget::Widget; - -#[derive(Clone)] -pub enum Child { - Widget(Widget), - RawBlock(syn::Block), -} - -impl ToTokens for Child { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - match self { - Self::Widget(widget) => widget.to_tokens(tokens), - Self::RawBlock(block) => { - let ts = if block.stmts.len() == 1 { - let first = &block.stmts[0]; - quote!(#first) - } else { - quote!(#block) - }; - ts.to_tokens(tokens); - } - } - } -} - -impl Parse for Child { - fn parse(input: ParseStream) -> Result<Self> { - match Widget::custom_parse(input, true) { - Ok(widget) => Ok(Self::Widget(widget)), - Err(_) => { - let block = input.parse::<syn::Block>()?; - Ok(Self::RawBlock(block)) - } - } - } -} - -pub fn walk_block_to_variable(block: &syn::Block) -> Option<proc_macro2::TokenStream> { - if let Some(statement) = block.stmts.first() { - return walk_statement(statement); - } - - return None; -} - -pub fn walk_statement(statement: &syn::Stmt) -> Option<proc_macro2::TokenStream> { - match statement { - syn::Stmt::Expr(expr) => match expr { - syn::Expr::Call(call) => Some(call.args.to_token_stream()), - syn::Expr::Path(path) => Some(path.to_token_stream()), - _ => None, - }, - _ => None, - } -} diff --git a/kayak_render_macros/src/children.rs b/kayak_render_macros/src/children.rs deleted file mode 100644 index b345753..0000000 --- a/kayak_render_macros/src/children.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::collections::HashSet; - -use crate::{ - attribute::Attribute, - child::{walk_block_to_variable, Child}, - get_core_crate, - widget_builder::build_widget_stream, -}; -use quote::{quote, ToTokens}; -use syn::parse::{Parse, ParseStream, Result}; - -#[derive(Clone)] -pub struct Children { - pub nodes: Vec<Child>, -} - -impl Children { - pub fn new(nodes: Vec<Child>) -> Self { - Children { nodes } - } - - pub fn get_clonable_attributes(&self, index: usize) -> Vec<proc_macro2::TokenStream> { - let mut tokens = Vec::new(); - - let regular_tokens: Vec<_> = match &self.nodes[index] { - Child::Widget(widget) => widget - .attributes - .attributes - .iter() - .filter_map(|attr| match attr { - Attribute::WithValue(_, block) => walk_block_to_variable(block), - _ => None, - }) - .collect(), - _ => vec![], - }; - tokens.extend(regular_tokens); - - let children_tokens: Vec<proc_macro2::TokenStream> = match &self.nodes[index] { - Child::Widget(widget) => (0..widget.children.nodes.len()) - .into_iter() - .map(|child_id| widget.children.get_clonable_attributes(child_id)) - .flatten() - .collect(), - _ => vec![], - }; - - tokens.extend(children_tokens); - - tokens.dedup_by(|a, b| a.to_string().eq(&b.to_string())); - - tokens - } - - pub fn as_option_of_tuples_tokens(&self) -> proc_macro2::TokenStream { - let kayak_core = get_core_crate(); - - let children_quotes: Vec<_> = self - .nodes - .iter() - .map(|child| { - quote! { #child } - }) - .collect(); - - match children_quotes.len() { - 0 => quote! { None }, - 1 => { - let child = if children_quotes[0].to_string() == "{ }" { - quote! { None } - } else { - let children_attributes: Vec<_> = self.get_clonable_attributes(0); - - // I think this is correct.. It needs more testing though.. - let clonable_children = children_attributes - .iter() - .filter(|ts| syn::parse_str::<syn::Path>(&ts.to_string()).is_ok()) - .collect::<Vec<_>>(); - - let cloned_attrs = quote! { - #(let #clonable_children = #clonable_children.clone();)*; - }; - if children_quotes[0].to_string() == "children" { - quote! { - #(#children_quotes)*.clone() - } - } else { - let children_builder = build_widget_stream( - quote! { child_widget }, - quote! { #(#children_quotes),* }, - 0, - ); - - quote! { - Some(#kayak_core::Children::new(move |parent_id: Option<#kayak_core::Index>, context: &mut #kayak_core::KayakContextRef| { - #cloned_attrs - #children_builder - context.commit(); - })) - } - } - }; - quote! { - #child - } - } - _ => { - // First get shared and non-shared attributes.. - let mut child_attributes_list = Vec::new(); - for i in 0..children_quotes.len() { - let ts_vec = self.get_clonable_attributes(i); - - // I think this is correct.. It needs more testing though.. - let clonable_children = ts_vec - .into_iter() - .filter(|ts| syn::parse_str::<syn::Path>(&ts.to_string()).is_ok()) - .collect::<Vec<_>>(); - - child_attributes_list.push(clonable_children); - } - - let mut all_attributes = HashSet::new(); - for child_attributes in child_attributes_list.iter() { - for child_attribute in child_attributes { - all_attributes.insert(child_attribute.to_string()); - } - } - - all_attributes.insert("children".to_string()); - - let base_matching: Vec<proc_macro2::TokenStream> = all_attributes - .iter() - .map(|a| format!("base_{}", a).to_string().parse().unwrap()) - .collect(); - - let all_attributes: Vec<proc_macro2::TokenStream> = - all_attributes.iter().map(|a| a.parse().unwrap()).collect(); - - let base_clone = quote! { - #(let #base_matching = #all_attributes.clone();)* - }; - - let base_clones_inner = quote! { - #(let #all_attributes = #base_matching.clone();)* - }; - - let mut output = Vec::new(); - output.push(quote! { #base_clone }); - for i in 0..children_quotes.len() { - output.push(quote! { #base_clones_inner }); - let name: proc_macro2::TokenStream = format!("child{}", i).parse().unwrap(); - let child = - build_widget_stream(quote! { #name }, children_quotes[i].clone(), i); - output.push(quote! { #child }); - } - - quote! { - Some(#kayak_core::Children::new(move |parent_id: Option<#kayak_core::Index>, context: &mut #kayak_core::KayakContextRef| { - #(#output)* - context.commit(); - })) - } - } - } - } -} - -impl Parse for Children { - fn parse(input: ParseStream) -> Result<Self> { - let mut nodes = vec![]; - - while !input.peek(syn::Token![<]) || !input.peek2(syn::Token![/]) { - let child = input.parse::<Child>()?; - nodes.push(child); - } - - Ok(Self::new(nodes)) - } -} - -impl ToTokens for Children { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.as_option_of_tuples_tokens().to_tokens(tokens); - } -} diff --git a/kayak_render_macros/src/function_component.rs b/kayak_render_macros/src/function_component.rs deleted file mode 100644 index d3f50c0..0000000 --- a/kayak_render_macros/src/function_component.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::get_core_crate; -use proc_macro::TokenStream; -use proc_macro2::Ident; -use proc_macro_error::emit_error; -use quote::{format_ident, quote}; -use syn::spanned::Spanned; -use syn::{parse_quote, FnArg, Pat, Signature, Type}; - -const DEFAULT_PROP_IDENT: &str = "__props"; - -pub struct WidgetArguments { - pub focusable: bool, -} - -impl Default for WidgetArguments { - fn default() -> Self { - Self { focusable: false } - } -} - -pub fn create_function_widget(f: syn::ItemFn, _widget_arguments: WidgetArguments) -> TokenStream { - let struct_name = f.sig.ident.clone(); - let (impl_generics, ty_generics, where_clause) = f.sig.generics.split_for_impl(); - - let (props, prop_type) = if let Some(parsed) = get_props(&f.sig) { - parsed - } else { - return TokenStream::new(); - }; - - let attrs = f.attrs; - let block = f.block; - let vis = f.vis; - - let kayak_core = get_core_crate(); - - TokenStream::from(quote! { - #(#attrs)* - #[derive(Default, Debug, PartialEq, Clone)] - #vis struct #struct_name #impl_generics { - pub id: #kayak_core::Index, - pub #props: #prop_type - } - - impl #impl_generics #kayak_core::Widget for #struct_name #ty_generics #where_clause { - - type Props = #prop_type; - - fn constructor(props: Self::Props) -> Self where Self: Sized { - Self { - id: #kayak_core::Index::default(), - #props: props, - } - } - - fn get_id(&self) -> #kayak_core::Index { - self.id - } - - fn set_id(&mut self, id: #kayak_core::Index) { - self.id = id; - } - - fn get_props(&self) -> &Self::Props { - &self.#props - } - - fn get_props_mut(&mut self) -> &mut Self::Props { - &mut self.#props - } - - fn render(&mut self, context: &mut #kayak_core::KayakContextRef) { - use #kayak_core::WidgetProps; - - let parent_id = Some(self.get_id()); - let children = self.#props.get_children(); - let mut #props = self.#props.clone(); - - #block - - self.#props = #props; - context.commit(); - } - } - }) -} - -fn get_props(signature: &Signature) -> Option<(Ident, Type)> { - if signature.inputs.len() > 1 { - let span = if signature.inputs.len() > 0 { - signature.inputs.span() - } else { - signature.span() - }; - emit_error!( - span, - "Functional widgets expect at most one argument (their props), but was given {}", - signature.inputs.len() - ); - return None; - } - - if signature.inputs.len() == 0 { - let ident = format_ident!("{}", DEFAULT_PROP_IDENT); - let ty: Type = parse_quote! {()}; - return Some((ident, ty)); - } - - match signature.inputs.first().unwrap() { - FnArg::Typed(typed) => { - let ident = match *typed.pat.clone() { - Pat::Ident(ident) => ident.ident, - err => { - emit_error!(err.span(), "Expected identifier, but got {:?}", err); - return None; - } - }; - - let ty = *typed.ty.clone(); - match &ty { - Type::Path(..) => {} - err => { - emit_error!(err.span(), "Invalid widget prop type: {:?}", err); - return None; - } - }; - - Some((ident, ty)) - } - FnArg::Receiver(receiver) => { - emit_error!(receiver.span(), "Functional widget cannot use 'self'"); - return None; - } - } -} diff --git a/kayak_render_macros/src/lib.rs b/kayak_render_macros/src/lib.rs deleted file mode 100644 index a6a9243..0000000 --- a/kayak_render_macros/src/lib.rs +++ /dev/null @@ -1,267 +0,0 @@ -extern crate proc_macro; - -mod function_component; -mod tags; - -mod attribute; -mod child; -mod children; -mod partial_eq; -mod use_effect; -mod widget; -mod widget_attributes; -mod widget_builder; -mod widget_props; - -use function_component::WidgetArguments; -use partial_eq::impl_dyn_partial_eq; -use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; -use quote::quote; -use syn::{parse_macro_input, parse_quote}; -use use_effect::UseEffect; -use widget::ConstructedWidget; - -use crate::widget::Widget; -use crate::widget_props::impl_widget_props; - -/// A top level macro that works the same as [`rsx`] but provides some additional -/// context for building the root widget. -#[proc_macro] -#[proc_macro_error] -pub fn render(input: TokenStream) -> TokenStream { - let widget = parse_macro_input!(input as Widget); - - let kayak_core = get_core_crate(); - - let result = quote! { - let mut context = #kayak_core::KayakContextRef::new(context, None); - let parent_id: Option<#kayak_core::Index> = None; - let children: Option<#kayak_core::Children> = None; - #widget - context.commit(); - }; - - TokenStream::from(result) -} - -/// A proc macro that turns RSX syntax into structure constructors and calls the -/// context to create the widgets. -#[proc_macro] -#[proc_macro_error] -pub fn rsx(input: TokenStream) -> TokenStream { - let widget = parse_macro_input!(input as Widget); - let result = quote! { #widget }; - TokenStream::from(result) -} - -/// A proc macro that turns RSX syntax into structure constructors only. -#[proc_macro] -#[proc_macro_error] -pub fn constructor(input: TokenStream) -> TokenStream { - let el = parse_macro_input!(input as ConstructedWidget); - let widget = el.widget; - let result = quote! { #widget }; - TokenStream::from(result) -} - -/// This attribute macro is what allows Rust functions to be generated into -/// valid widgets structs. -/// -/// # Examples -/// -/// ``` -/// #[widget] -/// fn MyWidget() { /* ... */ } -/// ``` -#[proc_macro_attribute] -#[proc_macro_error] -pub fn widget(args: TokenStream, item: TokenStream) -> TokenStream { - let mut widget_args = WidgetArguments::default(); - if !args.is_empty() { - // Parse stuff.. - let parsed = args.to_string(); - widget_args.focusable = parsed.contains("focusable"); - } - - let f = parse_macro_input!(item as syn::ItemFn); - function_component::create_function_widget(f, widget_args) -} - -/// A derive macro for the `WidgetProps` trait -#[proc_macro_derive(WidgetProps, attributes(prop_field))] -#[proc_macro_error] -pub fn derive_widget_props(item: TokenStream) -> TokenStream { - impl_widget_props(item) -} - -#[proc_macro_derive(DynPartialEq)] -pub fn dyn_partial_eq_macro_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).unwrap(); - - impl_dyn_partial_eq(&ast) -} - -#[proc_macro_attribute] -pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream { - let mut input = parse_macro_input!(input as syn::ItemTrait); - - let name = &input.ident; - - let bound: syn::TypeParamBound = parse_quote! { - DynPartialEq - }; - - input.supertraits.push(bound); - - (quote! { - #input - - impl core::cmp::PartialEq for Box<dyn #name> { - fn eq(&self, other: &Self) -> bool { - self.box_eq(other.as_any()) - } - } - }) - .into() -} - -/// Register some state data with an initial value. -/// -/// Once the state is created, this macro returns the current value, a closure for updating the current value, and -/// the raw Binding in a tuple. -/// -/// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-state.html), -/// upon which this macro is based. -/// -/// # Arguments -/// -/// * `initial_state`: The initial value for the state -/// -/// returns: (state, set_state, state_binding) -/// -/// # Examples -/// -/// ``` -/// # use kayak_core::{EventType, OnEvent}; -/// # use kayak_render_macros::use_state; -/// -/// let (count, set_count, ..) = use_state!(0); -/// -/// let on_event = OnEvent::new(move |_, event| match event.event_type { -/// EventType::Click(..) => { -/// set_count(foo + 1); -/// } -/// _ => {} -/// }); -/// -/// rsx! { -/// <> -/// <Button on_event={Some(on_event)}> -/// <Text size={16.0} content={format!("Count: {}", count)}>{}</Text> -/// </Button> -/// </> -/// } -/// ``` -#[proc_macro] -pub fn use_state(initial_state: TokenStream) -> TokenStream { - let initial_state = parse_macro_input!(initial_state as syn::Expr); - let kayak_core = get_core_crate(); - - let result = quote! {{ - use #kayak_core::{Bound, MutableBound}; - let state = context.create_state(#initial_state).unwrap(); - let cloned_state = state.clone(); - let set_state = move |value| { - cloned_state.set(value); - }; - - let state_value = state.get(); - - (state.get(), set_state, state) - }}; - TokenStream::from(result) -} - -/// Registers a side-effect callback for a given set of dependencies. -/// -/// This macro takes on the form: `use_effect!(callback, dependencies)`. The callback is -/// the closure that's ran whenever one of the Bindings in the dependencies array is changed. -/// -/// Dependencies are automatically cloned when added to the dependency array. This allows the -/// original bindings to be used within the callback without having to clone them manually first. -/// This can be seen in the example below where `count_state` is used within the callback and in -/// the dependency array. -/// -/// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-effect.html), -/// upon which this macro is based. -/// -/// # Arguments -/// -/// * `callback`: The side-effect closure -/// * `dependencies`: The dependency array (in the form `[dep_1, dep_2, ...]`) -/// -/// returns: () -/// -/// # Examples -/// -/// ``` -/// # use kayak_core::{EventType, OnEvent}; -/// # use kayak_render_macros::{use_effect, use_state}; -/// -/// let (count, set_count, count_state) = use_state!(0); -/// -/// use_effect!(move || { -/// println!("Count: {}", count_state.get()); -/// }, [count_state]); -/// -/// let on_event = OnEvent::new(move |_, event| match event.event_type { -/// EventType::Click(..) => { -/// set_count(foo + 1); -/// } -/// _ => {} -/// }); -/// -/// rsx! { -/// <> -/// <Button on_event={Some(on_event)}> -/// <Text size={16.0} content={format!("Count: {}", count)} /> -/// </Button> -/// </> -/// } -/// ``` -#[proc_macro] -pub fn use_effect(input: TokenStream) -> TokenStream { - let effect = parse_macro_input!(input as UseEffect); - effect.build() -} - -/// Helper method for getting the core crate -/// -/// Depending on the usage of the macro, this will become `crate`, `kayak_core`, -/// or `kayak_ui::core`. -/// -/// # Examples -/// -/// ``` -/// fn my_macro() -> proc_macro2::TokenStream { -/// let kayak_core = get_core_crate(); -/// quote! { -/// let foo = #kayak_core::Foo; -/// } -/// } -/// ``` -fn get_core_crate() -> proc_macro2::TokenStream { - let found_crate = proc_macro_crate::crate_name("kayak_core"); - if let Ok(found_crate) = found_crate { - match found_crate { - proc_macro_crate::FoundCrate::Itself => quote! { crate }, - proc_macro_crate::FoundCrate::Name(name) => { - let ident = syn::Ident::new(&name, proc_macro2::Span::call_site()); - quote!(#ident) - } - } - } else { - quote!(kayak_ui::core) - } -} diff --git a/kayak_render_macros/src/partial_eq.rs b/kayak_render_macros/src/partial_eq.rs deleted file mode 100644 index e4fccfb..0000000 --- a/kayak_render_macros/src/partial_eq.rs +++ /dev/null @@ -1,16 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; - -pub fn impl_dyn_partial_eq(ast: &syn::DeriveInput) -> TokenStream { - let name = &ast.ident; - let gen = quote! { - impl DynPartialEq for #name { - fn box_eq(&self, other: &dyn core::any::Any) -> bool { - other.downcast_ref::<Self>().map_or(false, |a| self == a) - } - } - }; - gen.into() -} diff --git a/kayak_render_macros/src/use_effect.rs b/kayak_render_macros/src/use_effect.rs deleted file mode 100644 index f09a151..0000000 --- a/kayak_render_macros/src/use_effect.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::get_core_crate; -use proc_macro::TokenStream; -use proc_macro2::Ident; -use quote::{format_ident, quote}; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::{Iter, Punctuated}; -use syn::{bracketed, Token}; - -pub(crate) struct UseEffect { - pub closure: syn::ExprClosure, - pub dependencies: Punctuated<Ident, Token![,]>, -} - -impl Parse for UseEffect { - fn parse(input: ParseStream) -> syn::Result<Self> { - let raw_deps; - let closure = input.parse()?; - let _: Token![,] = input.parse()?; - let _ = bracketed!(raw_deps in input); - let dependencies = raw_deps.parse_terminated(Ident::parse)?; - - Ok(Self { - closure, - dependencies, - }) - } -} - -impl UseEffect { - fn get_deps(&self) -> Iter<Ident> { - self.dependencies.iter() - } - - fn get_clone_dep_idents(&self) -> impl Iterator<Item = Ident> + '_ { - self.get_deps() - .map(|dep| format_ident!("{}_dependency_clone", dep)) - } - - fn create_clone_deps(&self) -> proc_macro2::TokenStream { - let deps = self.get_deps(); - let cloned_deps = self.get_clone_dep_idents(); - quote! { - #(let #cloned_deps = #deps.clone());* - } - } - - fn create_dep_array(&self) -> proc_macro2::TokenStream { - let cloned_deps = self.get_clone_dep_idents(); - quote! { - &[#(&#cloned_deps),*] - } - } - - /// Build the output token stream, creating the actual use_effect code - pub fn build(self) -> TokenStream { - let kayak_core = get_core_crate(); - - let dep_array = self.create_dep_array(); - let cloned_deps = self.create_clone_deps(); - let closure = self.closure; - let result = quote! {{ - use #kayak_core::{Bound, MutableBound}; - #cloned_deps; - context.create_effect( - #closure, - #dep_array - ); - }}; - TokenStream::from(result) - } -} diff --git a/kayak_render_macros/src/widget.rs b/kayak_render_macros/src/widget.rs deleted file mode 100644 index fede573..0000000 --- a/kayak_render_macros/src/widget.rs +++ /dev/null @@ -1,126 +0,0 @@ -use proc_macro2::TokenStream; -use quote::ToTokens; -use quote::{format_ident, quote}; -use syn::parse::{Parse, ParseStream, Result}; -use syn::Path; - -use crate::children::Children; -use crate::tags::ClosingTag; -use crate::widget_attributes::CustomWidgetAttributes; -use crate::widget_builder::build_widget_stream; -use crate::{get_core_crate, tags::OpenTag, widget_attributes::WidgetAttributes}; - -#[derive(Clone)] -pub struct Widget { - pub attributes: WidgetAttributes, - pub children: Children, - declaration: TokenStream, -} - -#[derive(Clone)] -pub struct ConstructedWidget { - pub widget: Widget, -} - -impl Parse for ConstructedWidget { - fn parse(input: ParseStream) -> Result<Self> { - Ok(Self { - widget: Widget::custom_parse(input, true).unwrap(), - }) - } -} - -impl Parse for Widget { - fn parse(input: ParseStream) -> Result<Self> { - Self::custom_parse(input, false) - } -} - -impl Widget { - pub fn is_custom_element(name: &syn::Path) -> bool { - match name.get_ident() { - None => true, - Some(ident) => { - let name = ident.to_string(); - let first_letter = name.get(0..1).unwrap(); - first_letter.to_uppercase() == first_letter - } - } - } - - pub fn custom_parse(input: ParseStream, as_prop: bool) -> Result<Widget> { - let open_tag = input.parse::<OpenTag>()?; - - let children = if open_tag.self_closing { - Children::new(vec![]) - } else { - let children = input.parse::<Children>()?; - let closing_tag = input.parse::<ClosingTag>()?; - closing_tag.validate(&open_tag); - children - }; - - let name = open_tag.name; - let declaration = if Self::is_custom_element(&name) { - let attrs = &open_tag.attributes.for_custom_element(&children); - let (props, constructor) = Self::construct(&name, attrs); - if !as_prop { - let widget_block = build_widget_stream(quote! { built_widget }, constructor, 0); - quote! {{ - #props - #widget_block - }} - } else { - quote! {{ - #props - let widget = #constructor; - widget - }} - } - } else { - panic!("Couldn't find widget!"); - }; - - Ok(Widget { - attributes: open_tag.attributes, - children, - declaration, - }) - } - - /// Constructs a widget and its props - /// - /// The returned tuple contains: - /// 1. The props constructor and assignment - /// 2. The widget constructor - /// - /// # Arguments - /// - /// * `name`: The full-path name of the widget - /// * `attrs`: The attributes (props) to apply to this widget - /// - /// returns: (TokenStream, TokenStream) - fn construct(name: &Path, attrs: &CustomWidgetAttributes) -> (TokenStream, TokenStream) { - let kayak_core = get_core_crate(); - - let prop_ident = format_ident!("internal_rsx_props"); - let attrs = attrs.assign_attributes(&prop_ident); - - let props = quote! { - let mut #prop_ident = <#name as #kayak_core::Widget>::Props::default(); - #attrs - }; - - let constructor = quote! { - <#name as #kayak_core::Widget>::constructor(#prop_ident) - }; - - (props, constructor) - } -} - -impl ToTokens for Widget { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.declaration.to_tokens(tokens); - } -} diff --git a/kayak_render_macros/src/widget_props.rs b/kayak_render_macros/src/widget_props.rs deleted file mode 100644 index 90ff6e4..0000000 --- a/kayak_render_macros/src/widget_props.rs +++ /dev/null @@ -1,156 +0,0 @@ -use proc_macro::TokenStream; - -use proc_macro2::Ident; -use proc_macro_error::emit_error; -use quote::quote; -use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Field, Meta, NestedMeta}; - -use crate::get_core_crate; - -/// The ident for the props helper attribute (`#[prop_field(Children)]`) -const PROPS_HELPER_IDENT: &str = "prop_field"; - -const PROP_CHILDREN: &str = "Children"; -const PROP_STYLE: &str = "Styles"; -const PROP_ON_EVENT: &str = "OnEvent"; -const PROP_ON_LAYOUT: &str = "OnLayout"; -const PROP_FOCUSABLE: &str = "Focusable"; - -#[derive(Default)] -struct PropsHelpers { - children_ident: Option<Ident>, - styles_ident: Option<Ident>, - on_event_ident: Option<Ident>, - on_layout_ident: Option<Ident>, - focusable_ident: Option<Ident>, -} - -pub(crate) fn impl_widget_props(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - .. - } = parse_macro_input!(input); - - let helpers = process_data(data); - - let set_children = if let Some(ident) = helpers.children_ident.clone() { - quote! { - self.#ident = children; - } - } else { - quote! {} - }; - let children_return = quote_clone_field(helpers.children_ident); - let styles_return = quote_clone_field(helpers.styles_ident); - let on_event_return = quote_clone_field(helpers.on_event_ident); - let on_layout_return = quote_clone_field(helpers.on_layout_ident); - let focusable_return = quote_clone_field(helpers.focusable_ident); - - let kayak_core = get_core_crate(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let output = quote! { - impl #impl_generics #kayak_core::WidgetProps for #ident #ty_generics #where_clause { - fn get_children(&self) -> Option<#kayak_core::Children> { - #children_return - } - - fn set_children(&mut self, children: Option<#kayak_core::Children>) { - #set_children - } - - fn get_styles(&self) -> Option<#kayak_core::styles::Style> { - #styles_return - } - - fn get_on_event(&self) -> Option<#kayak_core::OnEvent> { - #on_event_return - } - - fn get_on_layout(&self) -> Option<#kayak_core::OnLayout> { - #on_layout_return - } - - fn get_focusable(&self) -> Option<bool> { - #focusable_return - } - - } - }; - - output.into() -} - -/// Processes all fields of the given struct to collect the helper attribute data -/// -/// Attributes are processed in order and may overwrite previous attributes of the same type. This -/// results in the returned `PropsHelpers` reflecting the last instance of each respective helper. -fn process_data(data: Data) -> PropsHelpers { - let mut helpers = PropsHelpers::default(); - - match data { - Data::Struct(data) => { - for field in data.fields { - process_field(field, &mut helpers); - } - } - Data::Union(data) => { - for field in data.fields.named { - process_field(field, &mut helpers); - } - } - Data::Enum(data) => { - emit_error!(data.enum_token.span(), "Cannot derive WidgetProp for enum") - } - } - - helpers -} - -/// Process a field to collect the helper attribute -fn process_field(field: Field, props: &mut PropsHelpers) { - for attr in field.attrs { - if let Ok(meta) = attr.parse_meta() { - if let Meta::List(meta) = meta { - if !meta.path.is_ident(PROPS_HELPER_IDENT) { - continue; - } - - for nested in meta.nested { - let ident = match nested { - NestedMeta::Meta(meta) => meta.path().get_ident().cloned(), - err => { - emit_error!(err.span(), "Invalid attribute: {:?}", err); - None - } - }; - - if let Some(ident) = ident { - let ident_str = ident.to_string(); - match ident_str.as_str() { - PROP_CHILDREN => props.children_ident = field.ident.clone(), - PROP_STYLE => props.styles_ident = field.ident.clone(), - PROP_ON_EVENT => props.on_event_ident = field.ident.clone(), - PROP_ON_LAYOUT => props.on_layout_ident = field.ident.clone(), - PROP_FOCUSABLE => props.focusable_ident = field.ident.clone(), - err => emit_error!(err.span(), "Invalid attribute: {}", err), - } - } - } - } - } - } -} - -fn quote_clone_field(field_ident: Option<Ident>) -> proc_macro2::TokenStream { - if let Some(field_ident) = field_ident { - quote! { - self.#field_ident.clone() - } - } else { - quote! { - None - } - } -} diff --git a/kayak_render_macros/Cargo.toml b/kayak_ui_macros/Cargo.toml similarity index 59% rename from kayak_render_macros/Cargo.toml rename to kayak_ui_macros/Cargo.toml index 559d82c..3b234a3 100644 --- a/kayak_render_macros/Cargo.toml +++ b/kayak_ui_macros/Cargo.toml @@ -1,15 +1,13 @@ [package] -name = "kayak_render_macros" +name = "kayak_ui_macros" version = "0.1.0" edition = "2021" [lib] proc-macro = true -[features] -default = [] - [dependencies] +find-crate = "0.6" syn = { version = "1.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro2 = "1.0" @@ -17,5 +15,6 @@ proc-macro-error = "1.0" proc-macro-crate = "1.1" [dev-dependencies] -kayak_core = { path = "../kayak_core", version = "0.1.0" } +kayak_ui = { path = "../", version = "0.1.0" } pretty_assertions = "1.2.1" +bevy = { git = "https://github.com/bevyengine/bevy", rev="9423cb6a8d0c140e11364eb23c8feb7e576baa8c" } \ No newline at end of file diff --git a/kayak_render_macros/src/attribute.rs b/kayak_ui_macros/src/attribute.rs similarity index 95% rename from kayak_render_macros/src/attribute.rs rename to kayak_ui_macros/src/attribute.rs index 179a327..15d9d69 100644 --- a/kayak_render_macros/src/attribute.rs +++ b/kayak_ui_macros/src/attribute.rs @@ -1,95 +1,95 @@ -use quote::quote; -use std::hash::{Hash, Hasher}; - -use syn::{ - ext::IdentExt, - parse::{Parse, ParseStream, Result}, - spanned::Spanned, -}; - -pub type AttributeKey = syn::punctuated::Punctuated<syn::Ident, syn::Token![-]>; - -#[derive(Clone)] -pub enum Attribute { - Punned(AttributeKey), - WithValue(AttributeKey, syn::Block), -} - -impl Attribute { - pub fn ident(&self) -> &AttributeKey { - match self { - Self::Punned(ident) | Self::WithValue(ident, _) => ident, - } - } - - pub fn value_tokens(&self) -> proc_macro2::TokenStream { - match self { - Self::WithValue(_, value) => { - if value.stmts.len() == 1 { - let first = &value.stmts[0]; - quote!(#first) - } else { - quote!(#value) - } - } - Self::Punned(ident) => quote!(#ident), - } - } - - pub fn idents(&self) -> Vec<&syn::Ident> { - self.ident().iter().collect::<Vec<_>>() - } - - pub(crate) fn validate(self) -> Result<Self> { - if self.idents().len() < 2 { - Ok(self) - } else { - let alternative_name = self - .idents() - .iter() - .map(|x| x.to_string()) - .collect::<Vec<_>>() - .join("_"); - - let error_message = format!( - "Can't use dash-delimited values on custom widgets. Did you mean `{}`?", - alternative_name - ); - - Err(syn::Error::new(self.ident().span(), error_message)) - } - } -} - -impl PartialEq for Attribute { - fn eq(&self, other: &Self) -> bool { - let self_idents: Vec<_> = self.ident().iter().collect(); - let other_idents: Vec<_> = other.ident().iter().collect(); - self_idents == other_idents - } -} - -impl Eq for Attribute {} - -impl Hash for Attribute { - fn hash<H: Hasher>(&self, state: &mut H) { - let ident = self.idents(); - Hash::hash(&ident, state) - } -} - -impl Parse for Attribute { - fn parse(input: ParseStream) -> Result<Self> { - let name = AttributeKey::parse_separated_nonempty_with(input, syn::Ident::parse_any)?; - let not_punned = input.peek(syn::Token![=]); - - if !not_punned { - return Ok(Self::Punned(name)); - } - - input.parse::<syn::Token![=]>()?; - let value = input.parse::<syn::Block>()?; - - Ok(Self::WithValue(name, value)) - } -} +use quote::quote; +use std::hash::{Hash, Hasher}; + +use syn::{ + ext::IdentExt, + parse::{Parse, ParseStream, Result}, + spanned::Spanned, +}; + +pub type AttributeKey = syn::punctuated::Punctuated<syn::Ident, syn::Token![-]>; + +#[derive(Clone, Debug)] +pub enum Attribute { + Punned(AttributeKey), + WithValue(AttributeKey, syn::Block), +} + +impl Attribute { + pub fn ident(&self) -> &AttributeKey { + match self { + Self::Punned(ident) | Self::WithValue(ident, _) => ident, + } + } + + pub fn value_tokens(&self) -> proc_macro2::TokenStream { + match self { + Self::WithValue(_, value) => { + if value.stmts.len() == 1 { + let first = &value.stmts[0]; + quote!(#first) + } else { + quote!(#value) + } + } + Self::Punned(ident) => quote!(#ident), + } + } + + pub fn idents(&self) -> Vec<&syn::Ident> { + self.ident().iter().collect::<Vec<_>>() + } + + pub(crate) fn validate(self) -> Result<Self> { + if self.idents().len() < 2 { + Ok(self) + } else { + let alternative_name = self + .idents() + .iter() + .map(|x| x.to_string()) + .collect::<Vec<_>>() + .join("_"); + + let error_message = format!( + "Can't use dash-delimited values on custom widgets. Did you mean `{}`?", + alternative_name + ); + + Err(syn::Error::new(self.ident().span(), error_message)) + } + } +} + +impl PartialEq for Attribute { + fn eq(&self, other: &Self) -> bool { + let self_idents: Vec<_> = self.ident().iter().collect(); + let other_idents: Vec<_> = other.ident().iter().collect(); + self_idents == other_idents + } +} + +impl Eq for Attribute {} + +impl Hash for Attribute { + fn hash<H: Hasher>(&self, state: &mut H) { + let ident = self.idents(); + Hash::hash(&ident, state) + } +} + +impl Parse for Attribute { + fn parse(input: ParseStream) -> Result<Self> { + let name = AttributeKey::parse_separated_nonempty_with(input, syn::Ident::parse_any)?; + let not_punned = input.peek(syn::Token![=]); + + if !not_punned { + return Ok(Self::Punned(name)); + } + + input.parse::<syn::Token![=]>()?; + let value = input.parse::<syn::Block>()?; + + Ok(Self::WithValue(name, value)) + } +} diff --git a/kayak_ui_macros/src/block.rs b/kayak_ui_macros/src/block.rs new file mode 100644 index 0000000..07ed3ae --- /dev/null +++ b/kayak_ui_macros/src/block.rs @@ -0,0 +1,499 @@ +use syn::*; + + + +/// A braced block containing Rust statements. +/// +/// *This type is available only if Syn is built with the `"full"` feature.* +pub struct Block { + pub brace_token: token::Brace, + /// Statements in a block + pub stmts: Vec<Stmt>, +} + +/// A statement, usually ending in a semicolon. +/// +/// *This type is available only if Syn is built with the `"full"` feature.* +pub enum Stmt { + /// A local (let) binding. + Local(Local), + + /// An item definition. + Item(Item), + + /// Expr without trailing semicolon. + Expr(Expr), + + /// Expression with trailing semicolon. + Semi(Expr, Token![;]), +} + +/// A local `let` binding: `let x: u64 = s.parse()?`. +/// +/// *This type is available only if Syn is built with the `"full"` feature.* +pub struct Local { + pub attrs: Vec<Attribute>, + pub let_token: Token![let], + pub pat: Pat, + pub init: Option<(Token![=], Box<Expr>)>, + pub semi_token: Token![;], +} + +pub mod parsing { + use super::*; + use proc_macro2::{TokenStream, TokenTree, Delimiter}; + use syn::parse::discouraged::Speculative; + use syn::parse::{ParseStream, Parse, ParseBuffer}; + use syn::punctuated::Punctuated; + use syn::token::{Paren, Brace, Bracket}; + use syn::{Expr}; + + impl Block { + /// Parse the body of a block as zero or more statements, possibly + /// including one trailing expression. + /// + /// *This function is available only if Syn is built with the `"parsing"` + /// feature.* + /// + /// # Example + /// + /// ``` + /// use syn::{braced, token, Attribute, Block, Ident, Result, Stmt, Token}; + /// use syn::parse::{Parse, ParseStream}; + /// + /// // Parse a function with no generics or parameter list. + /// // + /// // fn playground { + /// // let mut x = 1; + /// // x += 1; + /// // println!("{}", x); + /// // } + /// struct MiniFunction { + /// attrs: Vec<Attribute>, + /// fn_token: Token![fn], + /// name: Ident, + /// brace_token: token::Brace, + /// stmts: Vec<Stmt>, + /// } + /// + /// impl Parse for MiniFunction { + /// fn parse(input: ParseStream) -> Result<Self> { + /// let outer_attrs = input.call(Attribute::parse_outer)?; + /// let fn_token: Token![fn] = input.parse()?; + /// let name: Ident = input.parse()?; + /// + /// let content; + /// let brace_token = braced!(content in input); + /// let inner_attrs = content.call(Attribute::parse_inner)?; + /// let stmts = content.call(Block::parse_within)?; + /// + /// Ok(MiniFunction { + /// attrs: { + /// let mut attrs = outer_attrs; + /// attrs.extend(inner_attrs); + /// attrs + /// }, + /// fn_token, + /// name, + /// brace_token, + /// stmts, + /// }) + /// } + /// } + /// ``` + pub fn parse_within(input: ParseStream) -> Result<Vec<Stmt>> { + let mut stmts = Vec::new(); + loop { + while let Some(semi) = input.parse::<Option<Token![;]>>()? { + stmts.push(Stmt::Semi(Expr::Verbatim(TokenStream::new()), semi)); + } + if input.is_empty() { + break; + } + let s = parse_stmt(input, true)?; + let requires_semicolon = if let Stmt::Expr(s) = &s { + requires_terminator(s) + } else { + false + }; + stmts.push(s); + if input.is_empty() { + break; + } else if requires_semicolon { + return Err(input.error("unexpected token")); + } + } + Ok(stmts) + } + } + + #[cfg_attr(doc_cfg, doc(cfg(feature = "parsing")))] + impl Parse for Block { + fn parse(input: ParseStream) -> Result<Self> { + let content; + Ok(Block { + brace_token: braced!(content in input), + stmts: content.call(Block::parse_within)?, + }) + } + } + + #[cfg_attr(doc_cfg, doc(cfg(feature = "parsing")))] + impl Parse for Stmt { + fn parse(input: ParseStream) -> Result<Self> { + parse_stmt(input, false) + } + } + + fn parse_stmt(input: ParseStream, allow_nosemi: bool) -> Result<Stmt> { + let begin = input.fork(); + let mut attrs = input.call(Attribute::parse_outer)?; + + // brace-style macros; paren and bracket macros get parsed as + // expression statements. + let ahead = input.fork(); + if let Ok(path) = ahead.call(Path::parse_mod_style) { + if ahead.peek(Token![!]) + && (ahead.peek2(token::Brace) + && !(ahead.peek3(Token![.]) || ahead.peek3(Token![?])) + || ahead.peek2(Ident)) + { + input.advance_to(&ahead); + return stmt_mac(input, attrs, path); + } + } + + if input.peek(Token![let]) { + stmt_local(input, attrs, begin) + } else if input.peek(Token![pub]) + || input.peek(Token![crate]) && !input.peek2(Token![::]) + || input.peek(Token![extern]) + || input.peek(Token![use]) + || input.peek(Token![static]) + && (input.peek2(Token![mut]) + || input.peek2(Ident) + && !(input.peek2(Token![async]) + && (input.peek3(Token![move]) || input.peek3(Token![|])))) + || input.peek(Token![const]) && !input.peek2(token::Brace) + || input.peek(Token![unsafe]) && !input.peek2(token::Brace) + || input.peek(Token![async]) + && (input.peek2(Token![unsafe]) + || input.peek2(Token![extern]) + || input.peek2(Token![fn])) + || input.peek(Token![fn]) + || input.peek(Token![mod]) + || input.peek(Token![type]) + || input.peek(Token![struct]) + || input.peek(Token![enum]) + || input.peek(Token![union]) && input.peek2(Ident) + || input.peek(Token![auto]) && input.peek2(Token![trait]) + || input.peek(Token![trait]) + || input.peek(Token![default]) + && (input.peek2(Token![unsafe]) || input.peek2(Token![impl])) + || input.peek(Token![impl]) + || input.peek(Token![macro]) + { + let mut item: Item = input.parse()?; + attrs.extend(replace_attrs_item(&mut item, Vec::new())); + replace_attrs_item(&mut item, attrs); + Ok(Stmt::Item(item)) + } else { + stmt_expr(input, allow_nosemi, attrs) + } + } + + fn stmt_mac(input: ParseStream, attrs: Vec<Attribute>, path: Path) -> Result<Stmt> { + let bang_token: Token![!] = input.parse()?; + let ident: Option<Ident> = input.parse()?; + let (delimiter, tokens) = parse_delimiter(input)?; + let semi_token: Option<Token![;]> = input.parse()?; + + Ok(Stmt::Item(Item::Macro(ItemMacro { + attrs, + ident, + mac: Macro { + path, + bang_token, + delimiter, + tokens, + }, + semi_token, + }))) + } + + fn stmt_local(input: ParseStream, attrs: Vec<Attribute>, begin: ParseBuffer) -> Result<Stmt> { + let let_token: Token![let] = input.parse()?; + + let mut pat: Pat = multi_pat_with_leading_vert(input)?; + if input.peek(Token![:]) { + let colon_token: Token![:] = input.parse()?; + let ty: Type = input.parse()?; + pat = Pat::Type(PatType { + attrs: Vec::new(), + pat: Box::new(pat), + colon_token, + ty: Box::new(ty), + }); + } + + let init = if input.peek(Token![=]) { + let eq_token: Token![=] = input.parse()?; + let init: Expr = input.parse()?; + + if input.peek(Token![else]) { + input.parse::<Token![else]>()?; + let content; + braced!(content in input); + content.call(Block::parse_within)?; + let verbatim = Expr::Verbatim(verbatim::between(begin, input)); + let semi_token: Token![;] = input.parse()?; + return Ok(Stmt::Semi(verbatim, semi_token)); + } + + Some((eq_token, Box::new(init))) + } else { + None + }; + + let semi_token: Token![;] = input.parse()?; + + Ok(Stmt::Local(Local { + attrs, + let_token, + pat, + init, + semi_token, + })) + } + + fn stmt_expr( + input: ParseStream, + allow_nosemi: bool, + mut attrs: Vec<Attribute>, + ) -> Result<Stmt> { + let mut e = expr_early(input)?; + + let mut attr_target = &mut e; + loop { + attr_target = match attr_target { + Expr::Assign(e) => &mut e.left, + Expr::AssignOp(e) => &mut e.left, + Expr::Binary(e) => &mut e.left, + _ => break, + }; + } + attrs.extend(replace_attrs(attr_target, Vec::new())); + replace_attrs(attr_target, attrs); + + if input.peek(Token![;]) { + return Ok(Stmt::Semi(e, input.parse()?)); + } + + if allow_nosemi || !requires_terminator(&e) { + Ok(Stmt::Expr(e)) + } else { + Err(input.error("expected semicolon")) + } + } + + fn requires_terminator(expr: &Expr) -> bool { + // see https://github.com/rust-lang/rust/blob/2679c38fc/src/librustc_ast/util/classify.rs#L7-L25 + match *expr { + Expr::Unsafe(..) + | Expr::Block(..) + | Expr::If(..) + | Expr::Match(..) + | Expr::While(..) + | Expr::Loop(..) + | Expr::ForLoop(..) + | Expr::Async(..) + | Expr::TryBlock(..) => false, + _ => true, + } + } + + fn replace_attrs(s: &mut Expr, new: Vec<Attribute>) -> Vec<Attribute> { + match s { + Expr::Box(ExprBox { attrs, .. }) + | Expr::Array(ExprArray { attrs, .. }) + | Expr::Call(ExprCall { attrs, .. }) + | Expr::MethodCall(ExprMethodCall { attrs, .. }) + | Expr::Tuple(ExprTuple { attrs, .. }) + | Expr::Binary(ExprBinary { attrs, .. }) + | Expr::Unary(ExprUnary { attrs, .. }) + | Expr::Lit(ExprLit { attrs, .. }) + | Expr::Cast(ExprCast { attrs, .. }) + | Expr::Type(ExprType { attrs, .. }) + | Expr::Let(ExprLet { attrs, .. }) + | Expr::If(ExprIf { attrs, .. }) + | Expr::While(ExprWhile { attrs, .. }) + | Expr::ForLoop(ExprForLoop { attrs, .. }) + | Expr::Loop(ExprLoop { attrs, .. }) + | Expr::Match(ExprMatch { attrs, .. }) + | Expr::Closure(ExprClosure { attrs, .. }) + | Expr::Unsafe(ExprUnsafe { attrs, .. }) + | Expr::Block(ExprBlock { attrs, .. }) + | Expr::Assign(ExprAssign { attrs, .. }) + | Expr::AssignOp(ExprAssignOp { attrs, .. }) + | Expr::Field(ExprField { attrs, .. }) + | Expr::Index(ExprIndex { attrs, .. }) + | Expr::Range(ExprRange { attrs, .. }) + | Expr::Path(ExprPath { attrs, .. }) + | Expr::Reference(ExprReference { attrs, .. }) + | Expr::Break(ExprBreak { attrs, .. }) + | Expr::Continue(ExprContinue { attrs, .. }) + | Expr::Return(ExprReturn { attrs, .. }) + | Expr::Macro(ExprMacro { attrs, .. }) + | Expr::Struct(ExprStruct { attrs, .. }) + | Expr::Repeat(ExprRepeat { attrs, .. }) + | Expr::Paren(ExprParen { attrs, .. }) + | Expr::Group(ExprGroup { attrs, .. }) + | Expr::Try(ExprTry { attrs, .. }) + | Expr::Async(ExprAsync { attrs, .. }) + | Expr::Await(ExprAwait { attrs, .. }) + | Expr::TryBlock(ExprTryBlock { attrs, .. }) + | Expr::Yield(ExprYield { attrs, .. }) => std::mem::replace(attrs, new), + Expr::Verbatim(_) => Vec::new(), + _ => unreachable!(), + } + } + + fn replace_attrs_item(i: &mut Item, new: Vec<Attribute>) -> Vec<Attribute> { + match i { + Item::ExternCrate(ItemExternCrate { attrs, .. }) + | Item::Use(ItemUse { attrs, .. }) + | Item::Static(ItemStatic { attrs, .. }) + | Item::Const(ItemConst { attrs, .. }) + | Item::Fn(ItemFn { attrs, .. }) + | Item::Mod(ItemMod { attrs, .. }) + | Item::ForeignMod(ItemForeignMod { attrs, .. }) + | Item::Type(ItemType { attrs, .. }) + | Item::Struct(ItemStruct { attrs, .. }) + | Item::Enum(ItemEnum { attrs, .. }) + | Item::Union(ItemUnion { attrs, .. }) + | Item::Trait(ItemTrait { attrs, .. }) + | Item::TraitAlias(ItemTraitAlias { attrs, .. }) + | Item::Impl(ItemImpl { attrs, .. }) + | Item::Macro(ItemMacro { attrs, .. }) + | Item::Macro2(ItemMacro2 { attrs, .. }) => std::mem::replace(attrs, new), + Item::Verbatim(_) => Vec::new(), + _ => unreachable!(), + } + } + + fn parse_delimiter(input: ParseStream) -> Result<(MacroDelimiter, TokenStream)> { + input.step(|cursor| { + if let Some((TokenTree::Group(g), rest)) = cursor.token_tree() { + let span = g.span(); + let delimiter = match g.delimiter() { + Delimiter::Parenthesis => MacroDelimiter::Paren(Paren(span)), + Delimiter::Brace => MacroDelimiter::Brace(Brace(span)), + Delimiter::Bracket => MacroDelimiter::Bracket(Bracket(span)), + Delimiter::None => { + return Err(cursor.error("expected delimiter")); + } + }; + Ok(((delimiter, g.stream()), rest)) + } else { + Err(cursor.error("expected delimiter")) + } + }) + } + + fn multi_pat_with_leading_vert(input: ParseStream) -> Result<Pat> { + let leading_vert: Option<Token![|]> = input.parse()?; + multi_pat_impl(input, leading_vert) + } + + fn multi_pat_impl(input: ParseStream, leading_vert: Option<Token![|]>) -> Result<Pat> { + let mut pat: Pat = input.parse()?; + if leading_vert.is_some() + || input.peek(Token![|]) && !input.peek(Token![||]) && !input.peek(Token![|=]) + { + let mut cases = Punctuated::new(); + cases.push_value(pat); + while input.peek(Token![|]) && !input.peek(Token![||]) && !input.peek(Token![|=]) { + let punct = input.parse()?; + cases.push_punct(punct); + let pat: Pat = input.parse()?; + cases.push_value(pat); + } + pat = Pat::Or(PatOr { + attrs: Vec::new(), + leading_vert, + cases, + }); + } + Ok(pat) + } + + fn expr_attrs(input: ParseStream) -> Result<Vec<Attribute>> { + let mut attrs = Vec::new(); + loop { + if input.peek(token::Group) { + let ahead = input.fork(); + let group = parse_group(&ahead)?; + if !group.content.peek(Token![#]) || group.content.peek2(Token![!]) { + break; + } + let attr = group.content.call(attr::parsing::single_parse_outer)?; + if !group.content.is_empty() { + break; + } + attrs.push(attr); + } else if input.peek(Token![#]) { + attrs.push(input.call(attr::parsing::single_parse_outer)?); + } else { + break; + } + } + Ok(attrs) + } + + fn expr_early(input: ParseStream) -> Result<Expr> { + let mut attrs = input.call(expr_attrs)?; + let mut expr = if input.peek(Token![if]) { + Expr::If(input.parse()?) + } else if input.peek(Token![while]) { + Expr::While(input.parse()?) + } else if input.peek(Token![for]) + && !(input.peek2(Token![<]) && (input.peek3(Lifetime) || input.peek3(Token![>]))) + { + Expr::ForLoop(input.parse()?) + } else if input.peek(Token![loop]) { + Expr::Loop(input.parse()?) + } else if input.peek(Token![match]) { + Expr::Match(input.parse()?) + } else if input.peek(Token![try]) && input.peek2(token::Brace) { + Expr::TryBlock(input.parse()?) + } else if input.peek(Token![unsafe]) { + Expr::Unsafe(input.parse()?) + } else if input.peek(Token![const]) { + Expr::Verbatim(input.call(expr_const)?) + } else if input.peek(token::Brace) { + Expr::Block(input.parse()?) + } else { + let allow_struct = AllowStruct(true); + let mut expr = unary_expr(input, allow_struct)?; + + attrs.extend(expr.replace_attrs(Vec::new())); + expr.replace_attrs(attrs); + + return parse_expr(input, expr, allow_struct, Precedence::Any); + }; + + if input.peek(Token![.]) && !input.peek(Token![..]) || input.peek(Token![?]) { + expr = trailer_helper(input, expr)?; + + attrs.extend(expr.replace_attrs(Vec::new())); + expr.replace_attrs(attrs); + + let allow_struct = AllowStruct(true); + return parse_expr(input, expr, allow_struct, Precedence::Any); + } + + attrs.extend(expr.replace_attrs(Vec::new())); + expr.replace_attrs(attrs); + Ok(expr) + } +} diff --git a/kayak_ui_macros/src/child.rs b/kayak_ui_macros/src/child.rs new file mode 100644 index 0000000..eeee1b7 --- /dev/null +++ b/kayak_ui_macros/src/child.rs @@ -0,0 +1,58 @@ +use quote::{quote, ToTokens}; +use syn::parse::{ParseStream, Result}; + +use crate::widget::Widget; + +#[derive(Clone, Debug)] +pub enum Child { + Widget((Widget, usize)), + RawBlock((syn::Block, usize)), +} + +impl Child { + pub fn custom_parse(input: ParseStream, index: usize) -> Result<Self> { + match Widget::custom_parse(input, true, index) { + Ok(widget) => Ok(Self::Widget((widget, index))), + Err(_) => { + let block = input.parse::<syn::Block>()?; + Ok(Self::RawBlock((block, index))) + } + } + } +} + +impl ToTokens for Child { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Widget((widget, _)) => widget.to_tokens(tokens), + Self::RawBlock((block, _)) => { + let ts = if block.stmts.len() == 1 { + let first = &block.stmts[0]; + quote!(#first) + } else { + quote!(#block) + }; + ts.to_tokens(tokens); + } + } + } +} + +// pub fn walk_block_to_variable(block: &syn::Block) -> Option<proc_macro2::TokenStream> { +// if let Some(statement) = block.stmts.first() { +// return walk_statement(statement); +// } + +// return None; +// } + +// pub fn walk_statement(statement: &syn::Stmt) -> Option<proc_macro2::TokenStream> { +// match statement { +// syn::Stmt::Expr(expr) => match expr { +// syn::Expr::Call(call) => Some(call.args.to_token_stream()), +// syn::Expr::Path(path) => Some(path.to_token_stream()), +// _ => None, +// }, +// _ => None, +// } +// } diff --git a/kayak_ui_macros/src/children.rs b/kayak_ui_macros/src/children.rs new file mode 100644 index 0000000..627821b --- /dev/null +++ b/kayak_ui_macros/src/children.rs @@ -0,0 +1,215 @@ +use crate::{child::Child, widget_builder::build_widget_stream}; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream, Result}; + +#[derive(Clone, Debug)] +pub struct Children { + pub nodes: Vec<Child>, +} + +impl Children { + pub fn new(nodes: Vec<Child>) -> Self { + Children { nodes } + } + + pub fn is_block(&self) -> bool { + self.nodes.iter().any(|node| match node { + Child::RawBlock(_) => true, + _ => false, + }) + } + + // pub fn get_clonable_attributes(&self, index: usize) -> Vec<proc_macro2::TokenStream> { + // let mut tokens = Vec::new(); + + // let regular_tokens: Vec<_> = match &self.nodes[index] { + // Child::Widget(widget) => widget + // .attributes + // .attributes + // .iter() + // .filter_map(|attr| match attr { + // Attribute::WithValue(_, block) => walk_block_to_variable(block), + // _ => None, + // }) + // .collect(), + // _ => vec![], + // }; + // tokens.extend(regular_tokens); + + // let children_tokens: Vec<proc_macro2::TokenStream> = match &self.nodes[index] { + // Child::Widget(widget) => (0..widget.children.nodes.len()) + // .into_iter() + // .map(|child_id| widget.children.get_clonable_attributes(child_id)) + // .flatten() + // .collect(), + // _ => vec![], + // }; + + // tokens.extend(children_tokens); + + // tokens.dedup_by(|a, b| a.to_string().eq(&b.to_string())); + + // tokens + // } + + pub fn as_option_of_tuples_tokens(&self, only_children: bool) -> proc_macro2::TokenStream { + let children_quotes: Vec<_> = self + .nodes + .iter() + .map(|child| { + let (entity_id, index) = match child { + Child::Widget((widget, index)) => (widget.entity_id.clone(), *index), + _ => (quote! {}, 0usize), + }; + ( + entity_id, + quote! { #child }, + match child { + Child::Widget(_) => true, + _ => false, + }, + index, + ) + }) + .collect(); + + match children_quotes.len() { + 0 => quote! { None }, + 1 => { + let child = if children_quotes[0].1.to_string() == "{ }" { + quote! { None } + } else { + // let children_attributes: Vec<_> = self.get_clonable_attributes(0); + + // I think this is correct.. It needs more testing though.. + // let clonable_children = children_attributes + // .iter() + // .filter(|ts| syn::parse_str::<syn::Path>(&ts.to_string()).is_ok()) + // .collect::<Vec<_>>(); + + // let cloned_attrs = quote! { + // #(let #clonable_children = #clonable_children.clone();)*; + // }; + let id = children_quotes[0].0.clone(); + let name: proc_macro2::TokenStream = format!("child{}", 0).parse().unwrap(); + let id = if id.to_string().contains("widget_entity") { + name + } else { + id + }; + let child_dec = children_quotes[0].1.clone(); + + if child_dec.to_string() == "children" { + quote! { + #child_dec + } + } else { + let children_builder = build_widget_stream( + quote! { #id }, + quote! { #child_dec }, + 0, + !only_children || children_quotes[0].2, + ); + + quote! { + // Some(#kayak_core::Children::new(move |parent_id: Option<bevy::prelude::Entity>, context: &mut #kayak_core::WidgetContext, commands: &mut bevy::prelude::Commands| { + // #cloned_attrs + #children_builder + // })) + } + } + }; + quote! { + #child + } + } + _ => { + // First get shared and non-shared attributes.. + // let mut child_attributes_list = Vec::new(); + // for i in 0..children_quotes.len() { + // let ts_vec = self.get_clonable_attributes(i); + + // // I think this is correct.. It needs more testing though.. + // let clonable_children = ts_vec + // .into_iter() + // .filter(|ts| syn::parse_str::<syn::Path>(&ts.to_string()).is_ok()) + // .collect::<Vec<_>>(); + + // child_attributes_list.push(clonable_children); + // } + + // let mut all_attributes = HashSet::new(); + // for child_attributes in child_attributes_list.iter() { + // for child_attribute in child_attributes { + // all_attributes.insert(child_attribute.to_string()); + // } + // } + + // all_attributes.insert("children".to_string()); + + // let base_matching: Vec<proc_macro2::TokenStream> = all_attributes + // .iter() + // .map(|a| format!("base_{}", a).to_string().parse().unwrap()) + // .collect(); + + // let all_attributes: Vec<proc_macro2::TokenStream> = + // all_attributes.iter().map(|a| a.parse().unwrap()).collect(); + + // let base_clone = quote! { + // #(let #base_matching = #all_attributes.clone();)* + // }; + + // let base_clones_inner = quote! { + // #(let #all_attributes = #base_matching.clone();)* + // }; + + let mut output = Vec::new(); + // output.push(quote! { #base_clone }); + for i in 0..children_quotes.len() { + // output.push(quote! { #base_clones_inner }); + let name: proc_macro2::TokenStream = format!("child{}", i).parse().unwrap(); + let entity_id = if children_quotes[i].0.to_string().contains("widget_entity") { + name + } else { + children_quotes[i].0.clone() + }; + let child = build_widget_stream( + quote! { #entity_id }, + children_quotes[i].1.clone(), + i, + children_quotes[i].2, + ); + + output.push(quote! { #child }); + } + + quote! { + // Some(#kayak_core::Children::new(move |parent_id: Option<bevy::prelude::Entity>, context: &mut #kayak_core::WidgetContext, commands: &mut bevy::prelude::Commands| { + #(#output)* + // context.commit(); + // })) + } + } + } + } +} + +impl Parse for Children { + fn parse(input: ParseStream) -> Result<Self> { + let mut nodes = vec![]; + let mut index: usize = 0; + while !input.peek(syn::Token![<]) || !input.peek2(syn::Token![/]) { + let child = Child::custom_parse(input, index)?; + nodes.push(child); + index += 1; + } + + Ok(Self::new(nodes)) + } +} + +impl ToTokens for Children { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.as_option_of_tuples_tokens(false).to_tokens(tokens); + } +} diff --git a/kayak_ui_macros/src/lib.rs b/kayak_ui_macros/src/lib.rs new file mode 100644 index 0000000..67394c1 --- /dev/null +++ b/kayak_ui_macros/src/lib.rs @@ -0,0 +1,77 @@ +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; +use quote::quote; +use syn::parse_macro_input; +use widget::{ConstructedWidget, Widget}; + +pub(crate) mod attribute; +pub(crate) mod child; +pub(crate) mod children; +pub(crate) mod tags; +pub(crate) mod widget; +pub(crate) mod widget_attributes; +pub(crate) mod widget_builder; +// mod block; + +/// A proc macro that turns RSX syntax into structure constructors and calls the +/// context to create the widgets. +#[proc_macro] +#[proc_macro_error] +pub fn rsx(input: TokenStream) -> TokenStream { + let widget = parse_macro_input!(input as Widget); + let result = quote! { #widget }; + TokenStream::from(result) +} + +/// A proc macro that turns RSX syntax into structure constructors and calls the +/// context to create the widgets. +#[proc_macro] +#[proc_macro_error] +pub fn constructor(input: TokenStream) -> TokenStream { + let el = parse_macro_input!(input as ConstructedWidget); + let widget = el.widget; + let result = quote! { + let widget_entity = #widget; + children.add(widget_entity); + }; + TokenStream::from(result) +} + +// / Helper method for getting the core crate +// / +// / Depending on the usage of the macro, this will become `crate`, `kayak_core`, +// / or `kayak_ui::core`. +// / +// / # Examples +// / +// / ``` +// / fn my_macro() -> proc_macro2::TokenStream { +// / let kayak_core = get_core_crate(); +// / quote! { +// / let foo = #kayak_core::Foo; +// / } +// / } +// / ``` +// fn get_core_crate() -> proc_macro2::TokenStream { +// let found_crate = proc_macro_crate::crate_name("kayak_ui"); +// if let Ok(found_crate) = found_crate { +// let result = match found_crate { +// proc_macro_crate::FoundCrate::Itself => { +// // let crate_name = find_crate::find_crate(|s| s == "kayak_ui" || s == "crate"); +// // dbg!(crate_name); +// // if path.contains("example") { +// //quote! { kayak_ui } +// // } else { +// quote! { crate } +// // } +// }, +// proc_macro_crate::FoundCrate::Name(name) => { +// let ident = syn::Ident::new(&name, proc_macro2::Span::call_site()); +// quote!(#ident) +// } +// }; +// result +// } else { +// quote!(kayak_ui) +// } +// } diff --git a/kayak_render_macros/src/tags.rs b/kayak_ui_macros/src/tags.rs similarity index 84% rename from kayak_render_macros/src/tags.rs rename to kayak_ui_macros/src/tags.rs index 6c877dd..7682f76 100644 --- a/kayak_render_macros/src/tags.rs +++ b/kayak_ui_macros/src/tags.rs @@ -1,4 +1,3 @@ -use crate::get_core_crate; use crate::widget_attributes::WidgetAttributes; use proc_macro_error::abort; use quote::quote; @@ -6,18 +5,18 @@ use syn::parse::{Parse, ParseStream, Result}; use syn::spanned::Spanned; pub struct OpenTag { - pub name: syn::Path, + pub name: Option<syn::Path>, pub attributes: WidgetAttributes, pub self_closing: bool, pub is_custom_element: bool, } -fn name_or_fragment(maybe_name: Result<syn::Path>) -> syn::Path { - let kayak_core = get_core_crate(); - - maybe_name.unwrap_or_else(|_| { - syn::parse_str::<syn::Path>(&format!("::{}::Fragment", kayak_core)).unwrap() - }) +fn name_or_fragment(maybe_name: Result<syn::Path>) -> Option<syn::Path> { + if let Ok(name) = maybe_name { + Some(name) + } else { + None + } } fn is_custom_element_name(path: &syn::Path) -> bool { @@ -36,7 +35,11 @@ impl Parse for OpenTag { input.parse::<syn::Token![<]>()?; let maybe_name = syn::Path::parse_mod_style(input); let name = name_or_fragment(maybe_name); - let is_custom_element = is_custom_element_name(&name); + let is_custom_element = if let Some(name) = &name { + is_custom_element_name(name) + } else { + false + }; let attributes = WidgetAttributes::custom_parse(input)?; let self_closing = input.parse::<syn::Token![/]>().is_ok(); input.parse::<syn::Token![>]>()?; @@ -51,7 +54,7 @@ impl Parse for OpenTag { } pub struct ClosingTag { - name: syn::Path, + name: Option<syn::Path>, } impl ClosingTag { diff --git a/kayak_ui_macros/src/widget.rs b/kayak_ui_macros/src/widget.rs new file mode 100644 index 0000000..c44898d --- /dev/null +++ b/kayak_ui_macros/src/widget.rs @@ -0,0 +1,235 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream, Result}; +use syn::Path; + +use crate::children::Children; +use crate::tags::ClosingTag; +use crate::widget_attributes::CustomWidgetAttributes; +use crate::widget_builder::build_widget_stream; +use crate::{tags::OpenTag, widget_attributes::WidgetAttributes}; + +#[derive(Clone, Debug)] +pub struct Widget { + pub attributes: WidgetAttributes, + pub children: Children, + declaration: TokenStream, + pub entity_id: TokenStream, +} + +#[derive(Clone)] +pub struct ConstructedWidget { + pub widget: Widget, +} + +impl Parse for ConstructedWidget { + fn parse(input: ParseStream) -> Result<Self> { + Ok(Self { + widget: Widget::custom_parse(input, true, 0).unwrap(), + }) + } +} + +impl Parse for Widget { + fn parse(input: ParseStream) -> Result<Self> { + Self::custom_parse(input, false, 0) + } +} + +impl Widget { + pub fn is_custom_element(name: &syn::Path) -> bool { + match name.get_ident() { + None => true, + Some(ident) => { + let name = ident.to_string(); + let first_letter = name.get(0..1).unwrap(); + first_letter.to_uppercase() == first_letter + } + } + } + + pub fn custom_parse(input: ParseStream, as_prop: bool, index: usize) -> Result<Widget> { + //let o = input.parse::<OpenCodeBlock>()?; + let open_tag = input.parse::<OpenTag>()?; + + let children = if open_tag.self_closing { + Children::new(vec![]) + } else { + let children = input.parse::<Children>()?; + let closing_tag = input.parse::<ClosingTag>()?; + closing_tag.validate(&open_tag); + children + }; + + let (entity_id, declaration) = if let Some(name) = open_tag.name { + if Self::is_custom_element(&name) { + let attrs = &open_tag.attributes.for_custom_element(&children); + let (entity_id, props, constructor) = + Self::construct(&name, attrs, as_prop, false, index); + if !as_prop { + let widget_block = + build_widget_stream(quote! { built_widget }, constructor, 0, false); + ( + entity_id, + quote! {{ + #props + #widget_block + }}, + ) + } else { + ( + entity_id.clone(), + quote! {{ + #props + #constructor; + #entity_id + }}, + ) + } + } else { + panic!("Couldn't find widget!"); + } + } else { + let attrs = &open_tag.attributes.for_custom_element(&children); + let name = syn::parse_str::<syn::Path>(&"fragment").unwrap(); + let (entity_id, props, _) = Self::construct(&name, attrs, true, true, index); + ( + entity_id, + quote! { + #props + }, + ) + }; + + Ok(Widget { + attributes: open_tag.attributes, + children, + declaration, + entity_id, + }) + } + + /// Constructs a widget and its props + /// + /// The returned tuple contains: + /// 1. The props constructor and assignment + /// 2. The widget constructor + /// + /// # Arguments + /// + /// * `name`: The full-path name of the widget + /// * `attrs`: The attributes (props) to apply to this widget + /// + /// returns: (TokenStream, TokenStream) + fn construct( + name: &Path, + attrs: &CustomWidgetAttributes, + as_prop: bool, + only_children: bool, + _index: usize, + ) -> (TokenStream, TokenStream, TokenStream) { + // let kayak_core = get_core_crate(); + + let entity_name_id = attrs.attributes.iter().find_map(|attribute| { + let key = attribute.ident(); + let value = attribute.value_tokens(); + let key_name = quote! { #key }.to_string(); + if key_name == "id" { + return Some(value); + } else { + None + } + }); + + let prop_ident = format_ident!("internal_rsx_props"); + let entity_id = if let Some(entity_name_id) = entity_name_id { + let entity_name_id = format_ident!("{}", entity_name_id.to_string().replace("\"", "")); + quote! { #entity_name_id } + } else { + quote! { widget_entity } + }; + let assigned_attrs = attrs.assign_attributes(&prop_ident); + + // If this widget contains children, add it (should result in error if widget does not accept children) + let children = if attrs.should_add_children() { + // let kayak_core = get_core_crate(); + let children_tuple = attrs + .children + .as_option_of_tuples_tokens(only_children || attrs.children.is_block()); + + // attrs.push(quote! { + // let children = children.clone(); + // #kayak_core::WidgetProps::set_children(&mut #ident, #children_tuple); + // }); + let start = if !only_children { + quote! { + let parent_id_old = parent_id; + let parent_id = Some(#entity_id); + let mut children = KChildren::new(); + } + } else { + quote! {} + }; + let middle = quote! { + #children_tuple + }; + let end = if !only_children { + quote! { + // #prop_ident.children.despawn(&mut commands); + #prop_ident.children = children; + let parent_id = parent_id_old; + } + } else { + quote! {} + }; + quote! { + #start + #middle + #end + } + } else { + quote! {} + }; + + if only_children { + return (entity_id, quote! { #children }, quote! {}); + } + + let props = quote! { + let entity = widget_context.get_child_at(parent_id); + let #entity_id = if let Some(entity) = entity { + use bevy::prelude::DespawnRecursiveExt; + commands.entity(entity).despawn_recursive(); + commands.get_or_spawn(entity).id() + } else { + commands.spawn_empty().id() + }; + let mut #prop_ident = #name { + #assigned_attrs + ..Default::default() + }; + + #children + }; + + let add_widget = if as_prop { + quote! {} + } else { + quote! { widget_context.add_widget(parent_id, #entity_id); } + }; + + let constructor = quote! { + commands.entity(#entity_id).insert(#prop_ident); + #add_widget + }; + + (entity_id, props, constructor) + } +} + +impl ToTokens for Widget { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.declaration.to_tokens(tokens); + } +} diff --git a/kayak_render_macros/src/widget_attributes.rs b/kayak_ui_macros/src/widget_attributes.rs similarity index 77% rename from kayak_render_macros/src/widget_attributes.rs rename to kayak_ui_macros/src/widget_attributes.rs index 1b83d2e..85679d1 100644 --- a/kayak_render_macros/src/widget_attributes.rs +++ b/kayak_ui_macros/src/widget_attributes.rs @@ -1,134 +1,128 @@ -use proc_macro2::{Ident, TokenStream}; -use proc_macro_error::emit_error; -use quote::quote; -use std::collections::HashSet; -use syn::{ - ext::IdentExt, - parse::{Parse, ParseStream, Result}, - spanned::Spanned, -}; - -use crate::child::Child; -use crate::{attribute::Attribute, children::Children, get_core_crate}; - -#[derive(Clone)] -pub struct WidgetAttributes { - pub attributes: HashSet<Attribute>, -} - -impl WidgetAttributes { - pub fn new(attributes: HashSet<Attribute>) -> Self { - Self { attributes } - } - - pub fn for_custom_element<'c>(&self, children: &'c Children) -> CustomWidgetAttributes<'_, 'c> { - CustomWidgetAttributes { - attributes: &self.attributes, - children, - } - } - - pub fn custom_parse(input: ParseStream) -> Result<Self> { - let mut parsed_self = input.parse::<Self>()?; - let new_attributes: HashSet<Attribute> = parsed_self - .attributes - .drain() - .filter_map(|attribute| match attribute.validate() { - Ok(x) => Some(x), - Err(err) => { - emit_error!(err.span(), "Invalid attribute: {}", err); - None - } - }) - .collect(); - - Ok(WidgetAttributes::new(new_attributes)) - } -} - -impl Parse for WidgetAttributes { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let mut attributes: HashSet<Attribute> = HashSet::new(); - while input.peek(syn::Ident::peek_any) { - let attribute = input.parse::<Attribute>()?; - let ident = attribute.ident(); - if attributes.contains(&attribute) { - emit_error!( - ident.span(), - "There is a previous definition of the {} attribute", - quote!(#ident) - ); - } - attributes.insert(attribute); - } - Ok(WidgetAttributes::new(attributes)) - } -} - -pub struct CustomWidgetAttributes<'a, 'c> { - attributes: &'a HashSet<Attribute>, - children: &'c Children, -} - -impl<'a, 'c> CustomWidgetAttributes<'a, 'c> { - /// Assign this widget's attributes to the given ident - /// - /// This takes the form: `IDENT.ATTR_NAME = ATTR_VALUE;` - /// - /// # Arguments - /// - /// * `ident`: The ident to assign to (i.e. "props") - /// - /// returns: TokenStream - pub fn assign_attributes(&self, ident: &Ident) -> TokenStream { - let mut attrs = self - .attributes - .iter() - .map(|attribute| { - let key = attribute.ident(); - let value = attribute.value_tokens(); - - quote! { - #ident.#key = #value; - } - }) - .collect::<Vec<_>>(); - - // If this widget contains children, add it (should result in error if widget does not accept children) - if self.should_add_children() { - let kayak_core = get_core_crate(); - let children_tuple = self.children.as_option_of_tuples_tokens(); - attrs.push(quote! { - let children = children.clone(); - #kayak_core::WidgetProps::set_children(&mut #ident, #children_tuple); - }); - } - - let result = quote! { - #( #attrs )* - }; - - result - } - - /// Determines whether `children` should be added to this widget or not - fn should_add_children(&self) -> bool { - if self.children.nodes.len() == 0 { - // No children - false - } else if self.children.nodes.len() == 1 { - let child = self.children.nodes.first().unwrap(); - match child { - Child::RawBlock(block) => { - // Is child NOT an empty block? (`<Foo>{}</Foo>`) - block.stmts.len() > 0 - } - // Child is a widget - _ => true, - } - } else { - // Multiple children - true - } - } -} +use proc_macro2::{Ident, TokenStream}; +use proc_macro_error::emit_error; +use quote::quote; +use std::collections::HashSet; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseStream, Result}, + spanned::Spanned, +}; + +use crate::child::Child; +use crate::{attribute::Attribute, children::Children}; + +#[derive(Clone, Debug)] +pub struct WidgetAttributes { + pub attributes: HashSet<Attribute>, +} + +impl WidgetAttributes { + pub fn new(attributes: HashSet<Attribute>) -> Self { + Self { attributes } + } + + pub fn for_custom_element<'c>(&self, children: &'c Children) -> CustomWidgetAttributes<'_, 'c> { + CustomWidgetAttributes { + attributes: &self.attributes, + children, + } + } + + pub fn custom_parse(input: ParseStream) -> Result<Self> { + let mut parsed_self = input.parse::<Self>()?; + let new_attributes: HashSet<Attribute> = parsed_self + .attributes + .drain() + .filter_map(|attribute| match attribute.validate() { + Ok(x) => Some(x), + Err(err) => { + emit_error!(err.span(), "Invalid attribute: {}", err); + None + } + }) + .collect(); + + Ok(WidgetAttributes::new(new_attributes)) + } +} + +impl Parse for WidgetAttributes { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let mut attributes: HashSet<Attribute> = HashSet::new(); + while input.peek(syn::Ident::peek_any) { + let attribute = input.parse::<Attribute>()?; + let ident = attribute.ident(); + if attributes.contains(&attribute) { + emit_error!( + ident.span(), + "There is a previous definition of the {} attribute", + quote!(#ident) + ); + } + attributes.insert(attribute); + } + Ok(WidgetAttributes::new(attributes)) + } +} + +pub struct CustomWidgetAttributes<'a, 'c> { + pub attributes: &'a HashSet<Attribute>, + pub children: &'c Children, +} + +impl<'a, 'c> CustomWidgetAttributes<'a, 'c> { + /// Assign this widget's attributes to the given ident + /// + /// This takes the form: `IDENT.ATTR_NAME = ATTR_VALUE;` + /// + /// # Arguments + /// + /// * `ident`: The ident to assign to (i.e. "props") + /// + /// returns: TokenStream + pub fn assign_attributes(&self, _ident: &Ident) -> TokenStream { + let attrs = self + .attributes + .iter() + .filter_map(|attribute| { + let key = attribute.ident(); + let value = attribute.value_tokens(); + let key_name = quote! { #key }.to_string(); + if key_name == "id" { + None + } else { + Some(quote! { + #key: #value, + }) + } + }) + .collect::<Vec<_>>(); + + let result = quote! { + #( #attrs )* + }; + + result + } + + /// Determines whether `children` should be added to this widget or not + pub fn should_add_children(&self) -> bool { + if self.children.nodes.len() == 0 { + // No children + false + } else if self.children.nodes.len() == 1 { + let child = self.children.nodes.first().unwrap(); + match child { + Child::RawBlock((block, _)) => { + // Is child NOT an empty block? (`<Foo>{}</Foo>`) + block.stmts.len() > 0 + } + // Child is a widget + _ => true, + } + } else { + // Multiple children + true + } + } +} diff --git a/kayak_render_macros/src/widget_builder.rs b/kayak_ui_macros/src/widget_builder.rs similarity index 66% rename from kayak_render_macros/src/widget_builder.rs rename to kayak_ui_macros/src/widget_builder.rs index ff2d105..c7db19b 100644 --- a/kayak_render_macros/src/widget_builder.rs +++ b/kayak_ui_macros/src/widget_builder.rs @@ -22,10 +22,19 @@ use quote::quote; pub fn build_widget_stream( widget_name: TokenStream, widget_constructor: TokenStream, - index: usize, + _index: usize, + with_children: bool, ) -> TokenStream { - quote! { - let #widget_name = #widget_constructor; - context.add_widget(#widget_name, #index); + if with_children { + quote! { + let #widget_name = #widget_constructor; + // context.add_widget(#widget_name, #index); + children.add(#widget_name); + } + } else { + quote! { + #widget_constructor; + // context.add_widget(#widget_name, #index); + } } } diff --git a/out.log b/out.log new file mode 100644 index 0000000000000000000000000000000000000000..5cc41701460d466c60aea52273161a784adb54d9 GIT binary patch literal 158970 zcmeI5ZEqY$lA!zZ0rwwh8Sv1a9eQT>+l<D5EZcwpW382Vvsf60D3PMZYwE=$Wm#+Z zuisrf6$(dYR%E?(bv5N~7;>|_vN9v%Jt8AB|KI<8zIwSjTOF*fR@bYytBciB`TLjE z|6cts`PBbDU46RxK>offEni5h$E)w<^Gbd>ZQkXLOL^}?o?Oc(y<N)_{{PRWKkJ+K zp2_boSLgEoH`33Ue19tKuCftd%XfO^y|dL{<-eD!SMrU&?#M4^tG{LKU#(uRo=AIt z@3pjlDp2tTa9_&Ed>`}Pm+zp<-?1^CNPj;G#u(w%yZOJCH=h1B=0!7Z{&oo|*uRwb z|Ji&#Z{Gc7^|fH~t^D#@=6@`IF_(Xl`Q4Y_t^{k>@()_1&;E~m=gHOT*J73i@u`fY z6ar=__M(CJa`mX8<nz^YdCqS<e~>%}gZ?z1k@bEh&^pfV2psfqzWPRfHF)zk;j2$& zR>$wS<cW-~bce5;)1aI2%wyrQ3;AXo1($$*_=9$g^i-af{QGI}i;oP|&Smu1()YFW z_>=s7A>ZizT>55wc=>zj`9OYsA<v%6U;6#q)ye9iJXh(t{|*{(2u+yLwY2zdb(Fzi zoZ!8+`?xK*`KyfcMFHO*<+r;6Ay|64`b@t4lJ%WRu3=bVHC!3&uYZFO{(3IG!2u?x z%1;OK+o{aqb%T{0JKF0DfdssPM>N2t^h6)33m5~slyrjwujEZIZ5;A1o%49pa0>6y zJ9A)E^oVi=G6`=0MX7^q=E@~+SAS?|S*LtXtsJ8%cjZqGR{z<+2z1{HzFx_H7s45e z(eGA=38%mz-s0m1a^s?_M&6+>Z};=N$%k_(Js}fN6>V}L8uDo;c3%qLgPk|>56nWz z7X{baY@M^aDfYGW^g{#1x5><78Tqc6b519f=C0Yjk=ZF9jN!9fuHmQ4PTbTa0DYjg zPI)hq03>VsoSW;AZmJo5QjOY01GpPYuY6;6ADrge;#_{glH@I32?dd9w8Vi(NQ6Ul z5KmLRAL);eEBtydgRpz{4+R#+dfbeM44n#poXfvc`K8-7<lOr<<f(s1&uU!`MZbS6 zT%YQbU$bu?Hd+Mz2;RT}P^cArD|Q=PfpsMJOl(Zr)A#ZQ+Q+O2`UcAi-uU}mo?kbA z!5dFR3*(z%TXJ5}-*S61tS_^jv1O~<t#hFk5(<2WtB)Fuej(qHq;rAzKz@7PSdD`Q z{&-ejHFH(0sZ~P9{;9E-b@(3$F7Rd46Nv4qX+iD#RCr&l4lveivAtuTZJYp;>(ztR z-+Lf!&yOBVdp{NKKw2&ZgNnIZN$<B>d}nZ#pD~;zhs+OaKEk^2=JLM@+~y;!3va3; z9yOFCzB-q`{w$O;_`A8Yq-34vV@^8zO)ESSKEk^vI(#HhV3jXK-~8p>H$28Gk%Gfa z|CHm`zQdiV-m0}Lf4{YIEZv&e$44>)<ixQ%4wjDxL7waMavt&1{MY>3CauD$P*&p# z_{FvURLfB7FQxcYI{_Z>JuaI6KP~K^;aRPT!;xAPw+L-Lhuy5(DC>O*8_<R=w-v*9 zHtr?Yv!+>wv|VS^Tw8XJnzvnNRM!`WExDhnRbx!0Bb9pBIWd<oZD-OKLAV`lQ~Hr5 z$F#E%<&2S$%O(!gj2t+-6pNx+1+r2XlG~yU{w?3WT>U6kZirr3QZwRDGq(uVX@2u* z<D<uYF0CP-3eHpAh__8%M<YA+y4&2YTGI9C6M2W6PM;r)FF}-*+h4NFL?M5de<c^8 z>ERnb$%3BB|7mk3e_zQro@m~Q9M-k;{8PhA<na!Fv+*An*-_Ku_L;&3M?P!3oXx@I z8193thAD8xvl7~re{6Q;=SLDrX@=-pFi0HZn9Au7g~$1uX|BsM3eYRnjrpALZ5g%A zWzl;+U*4OlzYQaw6rA{Hf#dHDAL^5Iv>}+aesld~zt%n<HKXEnsD>o(KV-Ccl>FEH zTdLI=uhi;&x;KT&vs>w}Hpd}3?0l9npQ#^5RN}e%!-nn})04Lu5`kMgc*^7L2=W|b z`L_~(ZJt%lnD)C2o@`x3Ga2Y&vO}7wv^9(?;V)b5D|1%l)zP1<;E@B<)fn`vX7|$^ z*wiffhte-rl*kc1o?^-PM(0s~lJ@}AWM9-?<(b0v4F4%(gAuZ}WF9qtgFVuD*VlA% zZ<KW%-UVZ)(vz)8u=YVl&ho=uvml<nl$MOHS!7_N&u7xBuA$hh+@e`N*4Ak9EGqq| z1OpS_fDp?H6?N4`bA~YmZFNdzE`KNW5!~({OW(2F8a<bYl+ZPJ1f`D1^@~ANezs+p z-b&4=xvou&lv+lov|Bd+?MS%IBZV@DT+YkB$MefM+G4S8T3V0x37*m{EZnHF{lh!X zqlN!U7D=<akz`n}wa;=i&t%_eJT_Kmbv;2nM5O3V;}8C2Vvfon`A9Mjx1<K4T!lu? z$$+tDPiD*{894AERf=<|Or=Vt^8@LdRj?^INg<rt$51}9v6hNQZM@X7k&&dT!|WP* zuFF!HzVsE+ZX4*U{`9OC<R{@}bPM#&bxf>9`z_eL-)xqyq<6!VueR51l<N`f6CSnN z5A2*-lD-*5JZShYSKQSzP<nu!7#m$S<&j7ZD;4=#PmW_EP}a+A4X5AYs7GKCUF!Ea zh7OY`Q8lgl0_I8P-cGJ{52V(HdXNj@Lb&7?`Io~?iwKkPuZG_T(%Ulpjl_`EV1-eA zL|sewm0I-xu{ErULFt&fH<{N{{!Hmv){aD!GMeO=LIXIC>N2JL(v7)(xp*X4S01~) z`bzBRf&Bl4R930anZ`h5tV=1<71OV7yyTn<<VavF8}YA3o#iCPXkE&A>YLU1sQyH& z(Nx2mEzo=xb(LCyR@PMFW1Tfm;YaKNobKK|+Mg_p#>IF|`E#(1hr*nw0>x7B*Bkjw zYk~L$jnD<RTZ2ae5BV&z81Cy~d9gfblDf^xz0h{OocRDlq$Q_KDi1f^deowdd8Hza zINPzYM!;6nrxEG5qWi6uF|T5~FFZx{CEVrNl-M&IY9kmMpIuh~7+HBU)#Lbmb#8J< z;G~Fqb3Vd;<0r5paa(EwA2)i`a6=S9&B-4c&i=Of3vO=5Q=0SnF448TrEOIT`=Vsa zyV=;9jWG|DD(Jo*Oj6YQ`9LI`x*^T(davW+yY@_TdRx>uE^BF!de)Q7o?5zLQ4s#u zxS6Cp<XHV%$Rh@yea1Fu*wc>!Zpm>NacMqc{w>Ww!k=FV|9WOLs@nRhk=5r-MHo3> z<Fz(*yVt%oD`fsp>Oq%2y4jvSDzza1H#IMPET+s$xFd7E5?QddN82%g^;E5=mV;;< z?PInc@AsvwUs?=_Jg2)CQh3@r&PM-G-uiO&vv9c9<>l45_4h2>;B`0X0DJ;qJZZf5 zC-RA{#`YgbW|uq!@h~0&zZ$Pl2b|AQ+u~n<8O;gPKQ$Fp?*28Cj5?Gj0>Mep_F%=l zZI{3g0?}Lf_k(ECG+&gC4ds7Hq=k94`Ga54@|yoF_dFQRE%x*Nb#2l6D>>8NM5>j> zZ~=Vn@1!;_<xz)+Iouc6eijU(&kki3@z3(#QB%KF=2_A)`TIb8dnf|dG&2O9`3o+^ zOFogO%CVaBP;bri*CzqAF?E=yj+xJP=v<C6xN@zX&g)3#o@*#@q`YW5FPK?dmhgRY z{cQc#crNPWzY3X9JA<_7%7*TYDWxE#fUne+t)Pthw^Y%x8-aOUO0B6$5pTS0==WH7 z;`b&~tUXVctM`G^+Ir-Xtg4KeGgTC<==@XU7Hx9#XB|mV8xJK8VyQMAb6aHMV0G`E z-qu>5?^pjLxKW#?dG*rL@hsA4j0GO?-q~XnLG1eNj#x@+SgBss{Ee@>Y1IR}=aAzU zBCEqj#D}tK*sF^60)nH)mS9P=DqHQWMy6(Uk)l}t)#t#>)dDc1Xcl-0L;&b0{;>u) z1ef#9?1{{5s1CQt44dtqnp@w}VuKyS5VrGJu#9KKe!^4nG@eN>_(MdIx<-IMhkez3 zs8B^SE;XMnA0g$k{0Vm3c5K<sF5AJR);b*lUh~R0U?ZM=Ag}`|_T#QV==(0=_WzK1 zJeH?@QEfDL^|*BIu|IRW8Btq&R#}HK+iO^c``Y+a{DWnkiTbNfg*kD4S8#GE|LKY! z`{nQreFT*ccnL^TJwvUULx`^hv{#ce+&72%KCjJCotjPR?(mONIfCxs+xA=Y^sv!8 z#M<r;=Xx%eqAvL|uJiekAJI{cW!zT{?H|Z*YH8tw&*gV^Ga^fT|5|8#F5hc=#gxOl zpjeX5YLA9#hkK1aVYRtmKR5;-3ALP>U7EqDdRy@KZ>xWm9NADU;ju-VUs5UD^9vZ* zoK<kD*XaDE$Tb`^p2k{-OQo#ULXkVZZdMbt#tNNmmcb;F^-~=&)gx_t>~b)y7psdh z?Vo!<sU40a-Fy&u(;%&3%su&`(!g5l&(d%1jaW9$Emt3u{hA7WskpSvFMb6cr{#ex z_J;4&N2zDk@!Pc;9PFZFbo~g5n{Ns9wpSPGQ_-(pr_alMW*d{d6)REk>TE<I^FzpS zS$2t-{^H%ts0cStioA)>4^M$-x@%TF1%=qx@URo?cr*Bb9q7mlD&;ihNa3BjF1~4N za{LbFKAYg4T#eK33YErrhgX6_yl~@zQX{Y`q4^Taw%K_w$U9>>?|^?jCxwWSC+HgJ zO-=<akNCtSS!YMop6aC9OvlUgY2$^$$3zxXwF05>kLpIEPd$r4D}i~=j5K>^IC41| zCjTDsph3V2d`gxXjBP@zd?WqqxfF4f(;vqS<w=`SjZQ>`x+CP-U|+=gBqyi2hdwN! z4WLf$!$#cJuaTF>9T^9mXAO(y&E?tK+$(o3Llc^R-EFfY44FKpz0}|uFNG>!#F<Dz zTHD|>cAcX!<McVCqp?R?Ph6?Z=IciG6}IDdZHIac!+k3<bVN7P{i=Eo^Y~QOe+^Gl zsE%~1eMot;Tlc-6Qf+&kA2l9{$zctb<0PB7153);8l2LS%II_{(O!qaJD&C>%pPg3 z53%=E!|xP$;nn$ZOs|{6aSpTNWgQ$V#NO9@IDRF55H`<F6;m&fZ11`Jj{k=>NY5f_ z$D8r_U}evdh3oeLjzzL+QJL*b!uO>P4{ws#O=A}FlNMd$C+hmX>191l5Iu<B%^TDJ z;frdd`C8s3_T<Dt>Icem7fXwO*kq)M3STwRUS3P6zw3@oJU;v|&AJh3<Zb<#w@+j1 z_8aL_eTTVP9?Wb=`#pQYj?iv@Mx#Zr3@5TK0v^p{*Sa_J&4HhDC&B*IIPWXLBh*xm z_c({|(2_Vt*8oZ`bKjyp-+EhW9d(5R>fD#VQ<*RiM0>0~WzFG!!z;}ET2}B`GlR3> zqqAbYs^lM!Wc2(rVsKv(*DTjF=}}kD(^VSYgpX5P9m#jY;d(QAD1B4k-=2+T18T9P z<s5uAx{J`D)!*%$#F8?l&S8Ekhl<;nugqUgwO373dhRZ@OGAB3=ztfc8i$N9UZ+Y% z9yKeLfZuX=P_}C|&9ir=t4c3?7c?o<cB~@Rv1X0ix#ldNyLOHALq4Xr?bDCQSUFOv zmHWp^Teo$1*|%x#{(h~c>Uhl*sl7CQh0E$3wK*M6g`sn{>Mzypsw4DFL~M>`6H?l! zorfbdM{6s`=CNpY7AyExW`_r%vYFN;@w@s2P|vjhdi+ik6~C0<$=jk?$(D9uDYt&e zyUs%OmUA-xhI&;`n#{67hfc`#cQlsix11Ypj)qHl*!A6ZwB0!FI+0wbuU?!8HQH+c zMz?XiRp+d2@YQgxBKzwYsqvUg%_c3U>9F^Wrwr~rKRuIp+pw`+UUHlMzHyU7zUL?S z$?>sWj#AI`ed8wcr}vi5VvF^;h&5j#f3Kd?9ipi(__FauHM68EW%{?r$)4jd4>I)( zBMUwzi)Q;=G-spL0Az~Pt9c??`lWc~?B5|K$8R=Rb)PHWHG;(}*9g^i>uJ{MQ+ZF< zb+x7Y__l_myX}adiHZ5g`a+pws?XH%=`h(0ZZ&ZjX98&}&6JR9yVd-{E`}7Jv;!9! z6UBK2%ViwD%e1t!6}Fz0`nqLKCcXK^>nVW8;(Yvi#ZerQx&NQ%r#$CX<KbG)YN*$y zP(dpw8?P0{kxZS6^(X26ZmkVSWr}(LtwBk1NV>-EnZgueILxyuCX--YGuLI?m<tU1 zY3ufFFwx$p+Z~n4Ln$zPRnzU_c$;Jzr>hU+R-??cYB3+7#<SyNi@7UkY^NBj0>i6k z&^Vl+l;;8Go8@p$nCh7r7u4RLhXb_t`S7Az9oSPX++MAU^p454_c0!>e8=$|=Z?3} zb@kJ`z7Sn(_fXi{egD1>+l_qFZ;K#SWPQnMv;0&6cDkVbb)`;s!*kxiw_$e9R$1)x zmf#Glk?(X#>;8wHj!#csAv{M9X=V8|D8{{IwYtOKLgV8nU5YOiuNNAg?emu4ns-WV zIQ)Hh8=rIh8(zyb{N1#QWB6NfZPS?dwGL^_8+hj2Ffk3{sLo{g==Qd2%Q5c)`E<DT z?I|OttAfMf&2#Nj-^;V9(|fTT+VmdW@0#9+TJACL*W#7oeRO%<V|uZ=i!tv+{@n2Q z$6{+ZZ+}<p6E&@CpJqk9R^RL>=klD%`V%oc>s@GRFwEbPkHIR~-{s#>JIh(=td~$Z zaMWvahuM;{&)E8|-&bmf*X?cU`(~S`yzf?e`rD<wp3;+9?seXE8$P9%)HY6eKep@B zdQuBN?cK=MPwC0*_PThneq2*cgQo2XtkchW(4^kM6ew~(Vf`L*k717f%y*chFZUwm z=uIu;w4Q37#gyK<e2i)RnYS_R?a1$#($jSO4wb1sPo(Z+Xq6%w1)E2HnYLt_HM^l_ zfY{j<>5Y0<5=Bt4ONF@JsAqRKdL9HXEzKhEE%k=tz4dt}C;zsYybkmA^XR|;`5pho zcjWloYG-?lx#=w*H$5G<e;o5Nhqtsh`f@Pj-b#+iA#G#GxW;kDY=%ioEt9idI8r}! zn~-c{Z1aDY9@~BA?TlRd-AhAZKKr)Db$@?p2vVPT>9ONDV|gevMp=H`D85-53X5sB z$p;35dtJ+8p&gTL3x@4sA&mu>o_!uiZg=!N2HiF+7Qb#A4v&4e4T;8e%R^C%xtE5d zD_&n77K`<lA34esEDgnWcyW0fk!J%eqRoHJzilRC;I}Ig`QB~T<@H<#e0Zw#`s4C3 znF1d#z9Xc6G{e>KmiES!e1XH8<A-nlES*dLI@Kof2@cB`IODaiY4G*s8P<m|9bpQL zkuF;osxfeMWh17-VSNsTqdPM(1q$zR91KHp6w~1;dmjp0SLR|0G}hnP@w+n^>p<c? z4~3~OmYo7k+UHO>hUYh?Kos{r9JYGaV;VHt-|+E=W<RDu6ZJk6Hp@q(G@nZsM}3cl ztK0jVf-i&0dmawcu*}IcsM>vxh0A9&9|aXE?|CRpefgCsI7<5*3P*pgWeOyDuhU@Z z&%CS;Sw6;8IJ>ekbHHf*Plc~LTQdiQKEhNO>#_43@M-VEVH=a(nFe2NgrP8w&HGG& zvTKataCYT{ra@&r4u@fAc4!(rQSU=xo0sp$@wr>Ax$L3$*f;+?KV(^s{MY=O-AUK3 zi@;;Kl>IDLIYV~Es%5zAgMDlFn>Os3;oFz1A0;~!XNmGrH<h{KPCv38!{LhdY>mlY zJ(W49xgP3({45|iAiZ<aZ@#56g#9bES{tL?@LFf;sHvp((fjjG(d=zx`=mNr&la+p z>o`Xgj~};Pf7~?l(?4?Bdfl=6MuXyQj9FHu_O?Br0-fhuqTbV|C1m=n%#74~DO*l~ z*76_GDE_R3)Sh^*qU(KcH3h;nDvd_TpO=uvS%avbxaAax&6kZw(Y!&MFXK~;<36Hj ze+r})2j@6!Yp$r>I^($wjpk>gd8=t-wWH~Hw0667#&f8<MoU|jJe$V2Q6K5^DUjxo zYcxXMV*N4l7_@)Hy!{;GcgbWvTR*{Sj&VI|95QQfJ;&JUDGwP}TTUCZ7Ujj`)>=;) zyDN5!M(=7rZG4N;;?ewRDdF+i5pge3<TVA_Iycyp63SFZwQYhwjiAXRjQpENIsI$I zb|%jQiF~e``((J=lv7^vJ3CJrU;D}Gzoi`~F?0G4Rt=ilnL|WFelsb*>eKwhM@|WK zxYB#A^ICn*42@<!=9DJ(GjRI_w<8lP*c~$EsZD-2PF^2hp6X@iquR`v-;dJ2o=~l0 zP%}U;oP5pmGugSxEuDHA$YpbC5*Q(N(worhv($UpxH(H9*0WITBq_C1<(=J*TVlg} zK2Y(np&aKvaKeYuMk%g0S>-2o+Kl00Jt^G#Pie;~Gujt)tKD;G=Y#0^8Pr*D!b=Wg zzI(#X#QG)V1&i7qN>9XVeetEWA5sYs&+`iz$@fOAzb|4_qogq)artJ(nZIX?4Cn3H zqry$}TgHj=j$yIjW_tD5ao+yZh-coO<2Z8eF?=<9KFNtA(0L)Z*lu(9w-MYwnR^E9 zcyTi$Y-$E+{5UV<c_g*TzVirivm;sTxY<!W_S@_@>g~@xMlG(JcO3jxy9s{SnT+_D zQ+%RLQ6rB0=Ec$`C_J|=$|FMad*fd%2E5mJ(0bcL8q0CADiMPE_jW>TJ&wC4JfyK9 zCma$F5bM}!hrVyzPwuz)hBN6{gX26Mzb)$|S>-ti<A6M_z<&7ntXFZn95E|1AWn$a zt@IhIR^8Etp1X@3yiTGhTQBN97q72xqg3p5<d`L&@tHuYE0si)Q6y<S+NYlN$9u#v z7Y$Cj-~6=~Bi6U!w|^}T*vGBj9cA_AANQQu6mz=4*-gQC6>oPaSDSyEVsXpuP%4dh zcav5*2D+)0w%rE3c9BAn&T|P`k_wh(*(ECYoFmDwr%O@6Gg7-kgLK9v1^n5v_%Abe zyG4uk>|?r=?UtgEWj=R>Dn8$m4)$be%zJit*JzN=yhaUgvlLy@tnRK*B%gChi}dMI zRESp~c7-0b*_V`w+bu^Us{`!L<yc1Geb2X84!s>?*puZjPyVeuQ-61)Bc6R*I-+(< z(a6^DQti1b6pH6w(kE)QG<AGUVAp7q&b&q$Z?ha-klFmsjoqP4H1{@b+O3wRj`PK? z(I%aFjWXV5DY~T95W7N=e9k2;(x*#NA+H?S6*`o&E-8^e-wrMEN}JuJOo@YTI_0gm zMKz17c9mj2|2}G2%WYB0YmIi5Rw)MhsO4?9MK9Ygvm2S4+y>b#>g_yE+-^A<)hZBo zN2cl+Xv<Ws?NapWs^r`iign|lq*+(%<*4R+8h1ncxwUC)Jb$(v{-fQXyCX00?AwwO zwOfityX76&Y)cyG?@Qs_vqrlj3+aqY-21aR@E=zyTTT8QIpgV-RB78j+<0I1PqN;| zzGL<nYpp(eou7vF;Qjlj|IyTizn1r@`mfdD`~L3735@K4hBtZoLiRV8`v|D)FQL?5 zsHV4_2CTm5X<YR8xPha--yq+69M!<*`vVN#x2xN$W7)rQu(~57JdpXG%RcQ_0>}5w zUh&(>uG!C|(tg-Zf@9hJ+$SYke@<M?oFB?)&*ksKWS;|llzays9yNQ)&*k%F0;%qq z9_BH0*S^oQd!c((-RZ>~$M0aU-pB7`us470rI59>&zrNe!61tDhCQ{<Zfbk=vF`S| z5UxUx4C5plBd(oz->F=~O(~VNTMx(C2E*mtR88yr_jF}nQcBkxJKdB`+uj7)?x~qm zvu!U^T5pqLwzptUF{PN}Ba%3Kx-E+NUW7fppaJK$s#j1_%b#tRR^|SLJ>86MZOp_p zEZc31p0>|nPr)OJMTy~w4@;WalWkMU_do3E<@8FiQlp%=*%l3_RQ_6w@_UT;a(t9@ zOP_9wV)1^8z2qH<fAaX<Ns8B!7rU0fUr9EttvBO#+oR{W9V)+P>9Y!<h-QkV?NKS( zr?Hnhq%OuxsXOC#+oET*uVYWmBUyiVy!yGdx1pqE)N0$5^wqdM9i5ITR%%rAHru0N zd%wt@icm4aOPh}ER@<he^VFV>PRA50H7a_WZP75@m$Ikck>p>_<v%<1u%ui1bXye5 zYl8PQJCexnv79G=*67b?K@6BbzZrDP_p|J&SW>aXPbrBxP4m__g~A@;?rSSbdO2?T zC~GZm3RS&Ac`vnXsZFudM_F%sQ)p|Iw|jXl<<&6LN7E@a%9iC^zYgk^_;1|7W7V8# zW_VB2F6^}FTEpbFWVLIr&R!EAH8sMubNt&m_AYF0kHTYjLYe0HJ+4l=b&RNInA#q# zqWwU7IYbU)%#<27ZnrIZntk6}Ny9`*ukCs^o+;eR*imzz;+c4P+-6wP$uYGFS&Dts z_}AKgF1;1<VD(HqRd#W)KkBNfVIbDkT~zFW!bYd}bJ}~aCF6A_IWO*nq@BMT?Styy z`*r-=4l=v*#cmU2FBiMdo;3Gea-$}D&i>S_19N8!x4BRw&we%Cjm91>Zr{Wc(LG+Y z%J*?`e~|CT;;tC(ykxH2+{xXU(LD0~Tu_<zR8>*^0p`Z;?CQTEM)!67soAgeP(~PX z-=yyv@;T<ay#5x>&wLqsKf*!IXZbgyS$gkdO;;Favi~jRuz08895=u!?{YgTH^97U zc%GFkjn%l5=0IeDPu+tU&3-&5>!|QB_t(B|`af9RYWUc1^t>fwquptzJ1^n;myK)! zZ?vm%o!yFh^AQ!DNO*mwssXN~-G#scw#aVVoSg56$V-mv-}im1^WOZG?kGuj%x!X% zbl=hZ<JfMCd3*Mq0rh_G2@g|ONo;dH@a`(@x2#sLk1^$DI;*a(_c{e~>(wiN=j}hO zP_6eo1s`$M<lJMRlgcxr>tjrzfUO<Wd!2%vs3K}JBe*X+cmLo%Kh<_KBW!90=}P>( zkms52P4=CyT5fiv%}7FyE6ypfywqlAqp|qhW7IO<^N!P%d99D+=W`r3r&d^|_pW${ z&znk4ej&2@QoeDX#alU(qWg|Vi#Rq#wK&67pW_r=ya`U2TW95QnqfWvHUH+Z+#R7R zk+en≪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 literal 0 HcmV?d00001 diff --git a/src/calculate_nodes.rs b/src/calculate_nodes.rs new file mode 100644 index 0000000..d7c5902 --- /dev/null +++ b/src/calculate_nodes.rs @@ -0,0 +1,321 @@ +use bevy::{ + prelude::{Assets, Commands, Entity, Query, Res, ResMut, With}, + utils::HashMap, +}; +use kayak_font::KayakFont; + +use crate::{ + layout::{DataCache, Rect}, + node::{DirtyNode, Node, NodeBuilder, WrappedIndex}, + prelude::{Context, KStyle}, + render::font::FontMapping, + render_primitive::RenderPrimitive, + styles::{StyleProp, Units}, +}; + +pub fn calculate_nodes( + mut commands: Commands, + mut context: ResMut<Context>, + fonts: Res<Assets<KayakFont>>, + font_mapping: Res<FontMapping>, + query: Query<Entity, With<DirtyNode>>, + all_styles_query: Query<&KStyle>, + node_query: Query<(Entity, &Node)>, + nodes_no_entity_query: Query<&'static Node>, +) { + let mut new_nodes = HashMap::<Entity, (Node, bool)>::default(); + // This is the maximum recursion depth for this method. + // Recursion involves recalculating layout which should be done sparingly. + // const MAX_RECURSION_DEPTH: usize = 2; + + context.current_z = 0.0; + + let initial_styles = KStyle::initial(); + let default_styles = KStyle::new_default(); + + // Jump out early. + // if query.is_empty() { + // return; + // } + if let Ok(tree) = context.tree.clone().read() { + for dirty_entity in query.iter() { + let dirty_entity = WrappedIndex(dirty_entity); + let styles = all_styles_query + .get(dirty_entity.0) + .unwrap_or(&default_styles); + // Get the parent styles. Will be one of the following: + // 1. Already-resolved node styles (best) + // 2. Unresolved widget prop styles + // 3. Unresolved default styles + let parent_styles = if let Some(parent_widget_id) = tree.parents.get(&dirty_entity) { + if let Ok((_, parent_node)) = node_query.get(parent_widget_id.0) { + parent_node.resolved_styles.clone() + } else if let Some(parent_node) = new_nodes.get(&parent_widget_id.0) { + parent_node.0.resolved_styles.clone() + } else if let Ok(parent_styles) = all_styles_query.get(parent_widget_id.0) { + parent_styles.clone() + } else { + default_styles.clone() + } + } else { + default_styles.clone() + }; + + let parent_z = if let Some(parent_widget_id) = tree.parents.get(&dirty_entity) { + if let Ok((_, parent_node)) = node_query.get(parent_widget_id.0) { + parent_node.z + } else if let Some(parent_node) = new_nodes.get(&parent_widget_id.0) { + parent_node.0.z + } else { + -1.0 + } + } else { + -1.0 + }; + + let current_z = { + if parent_z > -1.0 { + parent_z + 1.0 + } else { + let z = context.current_z; + context.current_z += 1.0; + z + } + }; + + let raw_styles = styles.clone(); + let mut styles = raw_styles.clone(); + // Fill in all `initial` values for any unset property + styles.apply(&initial_styles); + // Fill in all `inherited` values for any `inherit` property + styles.inherit(&parent_styles); + + let (primitive, needs_layout) = create_primitive( + &mut commands, + &context, + &fonts, + &font_mapping, + // &node_query, + dirty_entity, + &mut styles, + ); + + let children = tree.children.get(&dirty_entity).cloned().unwrap_or(vec![]); + + let width = styles.width.resolve().value_or(0.0, 0.0); + let height = styles.height.resolve().value_or(0.0, 0.0); + + let mut node = NodeBuilder::empty() + .with_id(dirty_entity) + .with_styles(styles, Some(raw_styles)) + .with_children(children) + .with_primitive(primitive) + .build(); + + if dirty_entity == tree.root_node.unwrap() { + if let Ok(mut cache) = context.layout_cache.try_write() { + cache.rect.insert( + dirty_entity, + Rect { + posx: 0.0, + posy: 0.0, + width, + height, + z_index: 0.0, + }, + ); + } + } + node.z = current_z; + new_nodes.insert(dirty_entity.0, (node, needs_layout)); + } + + // let has_new_nodes = new_nodes.len() > 0; + + for (entity, (node, needs_layout)) in new_nodes.drain() { + commands.entity(entity).insert(node); + if !needs_layout { + commands.entity(entity).remove::<DirtyNode>(); + } + } + + // if has_new_nodes { + // build_nodes_tree(&mut context, &tree, &node_query); + // } + + // dbg!("STARTING MORPHORM CALC!"); + // dbg!("node_tree"); + // context.node_tree.dump(); + // if let Ok(tree) = context.tree.try_read() { + // dbg!("tree"); + // dbg!(&tree); + // tree.dump(); + // } + { + let context = context.as_mut(); + if let Ok(tree) = context.tree.try_read() { + let node_tree = &*tree; + if let Ok(mut cache) = context.layout_cache.try_write() { + let mut data_cache = DataCache { + cache: &mut cache, + query: &nodes_no_entity_query, + }; + + // dbg!(&node_tree); + + morphorm::layout(&mut data_cache, node_tree, &nodes_no_entity_query); + } + } + } + // dbg!("FINISHED MORPHORM CALC!"); + } +} + +fn create_primitive( + commands: &mut Commands, + context: &Context, + fonts: &Assets<KayakFont>, + font_mapping: &FontMapping, + // query: &Query<(Entity, &Node)>, + id: WrappedIndex, + styles: &mut KStyle, +) -> (RenderPrimitive, bool) { + let mut render_primitive = RenderPrimitive::from(&styles.clone()); + let mut needs_layout = false; + + match &mut render_primitive { + RenderPrimitive::Text { + content, + font, + properties, + text_layout, + .. + } => { + // --- Bind to Font Asset --- // + let font_handle = font_mapping.get_handle(font.clone()).unwrap(); + if let Some(font) = fonts.get(&font_handle) { + // self.bind(id, &asset); + if let Ok(node_tree) = context.tree.try_read() { + if let Some(parent_id) = node_tree.get_parent(id) { + if let Some(parent_layout) = context.get_layout(&parent_id) { + properties.max_size = (parent_layout.width, parent_layout.height); + + // --- Calculate Text Layout --- // + *text_layout = font.measure(&content, *properties); + let measurement = text_layout.size(); + + // --- Apply Layout --- // + if matches!(styles.width, StyleProp::Default) { + styles.width = StyleProp::Value(Units::Pixels(measurement.0)); + } + if matches!(styles.height, StyleProp::Default) { + styles.height = StyleProp::Value(Units::Pixels(measurement.1)); + } + } else { + needs_layout = true; + } + } else { + needs_layout = true; + } + } else { + needs_layout = true; + } + } else { + needs_layout = true; + } + } + _ => {} + } + + if needs_layout { + commands.entity(id.0).insert(DirtyNode); + } + + (render_primitive, needs_layout) +} + +// pub fn build_nodes_tree(context: &mut Context, tree: &Tree, node_query: &Query<(Entity, &Node)>) { +// if tree.root_node.is_none() { +// return; +// } +// let mut node_tree = Tree::default(); +// node_tree.root_node = tree.root_node; +// node_tree.children.insert( +// tree.root_node.unwrap(), +// get_valid_node_children(&tree, &node_query, tree.root_node.unwrap()), +// ); + +// // let old_focus = self.focus_tree.current(); +// // self.focus_tree.clear(); +// // self.focus_tree.add(root_node_id, &self.tree); + +// for (node_id, node) in node_query.iter() { +// let node_id = WrappedIndex(node_id); +// if let Some(widget_styles) = node.raw_styles.as_ref() { +// // Only add widgets who have renderable nodes. +// // if widget_styles.render_command.resolve() != RenderCommand::Empty { +// let valid_children = get_valid_node_children(&tree, &node_query, node_id); +// node_tree.children.insert(node_id, valid_children); +// let valid_parent = get_valid_parent(&tree, &node_query, node_id); +// if let Some(valid_parent) = valid_parent { +// node_tree.parents.insert(node_id, valid_parent); +// } +// // } +// } + +// // let focusable = self.get_focusable(widget_id).unwrap_or_default(); +// // if focusable { +// // self.focus_tree.add(widget_id, &self.tree); +// // } +// } + +// // if let Some(old_focus) = old_focus { +// // if self.focus_tree.contains(old_focus) { +// // self.focus_tree.focus(old_focus); +// // } +// // } + +// // dbg!(&node_tree); + +// // context.node_tree = node_tree; +// } + +// pub fn get_valid_node_children( +// tree: &Tree, +// query: &Query<(Entity, &Node)>, +// node_id: WrappedIndex, +// ) -> Vec<WrappedIndex> { +// let mut children = Vec::new(); +// if let Some(node_children) = tree.children.get(&node_id) { +// for child_id in node_children { +// if let Ok((_, _child_node)) = query.get(child_id.0) { +// // if child_node.resolved_styles.render_command.resolve() != RenderCommand::Empty { +// children.push(*child_id); +// // } else { +// // children.extend(get_valid_node_children(tree, query, *child_id)); +// // } +// } else { +// // children.extend(get_valid_node_children(tree, query, *child_id)); +// } +// } +// } + +// children +// } + +// pub fn get_valid_parent( +// tree: &Tree, +// query: &Query<(Entity, &Node)>, +// node_id: WrappedIndex, +// ) -> Option<WrappedIndex> { +// if let Some(parent_id) = tree.parents.get(&node_id) { +// if let Ok((_, parent_node)) = query.get(parent_id.0) { +// // if parent_node.resolved_styles.render_command.resolve() != RenderCommand::Empty { +// return Some(*parent_id); +// // } +// } +// // return get_valid_parent(tree, query, *parent_id); +// } + +// None +// } diff --git a/bevy_kayak_renderer/src/camera/camera.rs b/src/camera/camera.rs similarity index 93% rename from bevy_kayak_renderer/src/camera/camera.rs rename to src/camera/camera.rs index a3e5ba4..197976c 100644 --- a/bevy_kayak_renderer/src/camera/camera.rs +++ b/src/camera/camera.rs @@ -1,76 +1,75 @@ -use bevy::{ - ecs::query::QueryItem, - prelude::{Bundle, Component, GlobalTransform, Transform, With}, - render::{ - camera::{Camera, CameraProjection, CameraRenderGraph, DepthCalculation, WindowOrigin}, - extract_component::ExtractComponent, - primitives::Frustum, - view::VisibleEntities, - }, -}; - -use super::ortho::UIOrthographicProjection; - -#[derive(Component, Clone, Default)] -pub struct CameraUiKayak; - -impl ExtractComponent for CameraUiKayak { - type Query = &'static Self; - type Filter = With<Camera>; - - fn extract_component(item: QueryItem<Self::Query>) -> Self { - item.clone() - } -} - -#[derive(Bundle)] -pub struct UICameraBundle { - pub camera: Camera, - pub camera_render_graph: CameraRenderGraph, - pub orthographic_projection: UIOrthographicProjection, - pub visible_entities: VisibleEntities, - pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, - pub marker: CameraUiKayak, -} - -impl UICameraBundle { - pub const UI_CAMERA: &'static str = "KAYAK_UI_CAMERA"; - pub fn new() -> Self { - // we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset - // the camera's translation by far and use a right handed coordinate system - let far = 1000.0; - - let orthographic_projection = UIOrthographicProjection { - far, - depth_calculation: DepthCalculation::ZDifference, - window_origin: WindowOrigin::BottomLeft, - ..Default::default() - }; - - let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); - - let view_projection = - orthographic_projection.get_projection_matrix() * transform.compute_matrix().inverse(); - let frustum = Frustum::from_view_projection( - &view_projection, - &transform.translation, - &transform.back(), - orthographic_projection.far(), - ); - UICameraBundle { - camera: Camera { - priority: isize::MAX - 1, - ..Default::default() - }, - camera_render_graph: CameraRenderGraph::new(crate::render::draw_ui_graph::NAME), - orthographic_projection, - frustum, - visible_entities: VisibleEntities::default(), - transform, - global_transform: Default::default(), - marker: CameraUiKayak, - } - } -} +use bevy::{ + ecs::query::QueryItem, + prelude::{Bundle, Component, GlobalTransform, Transform, With}, + render::{ + camera::{Camera, CameraProjection, CameraRenderGraph, WindowOrigin}, + extract_component::ExtractComponent, + primitives::Frustum, + view::VisibleEntities, + }, +}; + +use super::ortho::UIOrthographicProjection; + +#[derive(Component, Clone, Default)] +pub struct CameraUiKayak; + +impl ExtractComponent for CameraUiKayak { + type Query = &'static Self; + type Filter = With<Camera>; + + fn extract_component(item: QueryItem<Self::Query>) -> Self { + item.clone() + } +} + +#[derive(Bundle)] +pub struct UICameraBundle { + pub camera: Camera, + pub camera_render_graph: CameraRenderGraph, + pub orthographic_projection: UIOrthographicProjection, + pub visible_entities: VisibleEntities, + pub frustum: Frustum, + pub transform: Transform, + pub global_transform: GlobalTransform, + pub marker: CameraUiKayak, +} + +impl UICameraBundle { + pub const UI_CAMERA: &'static str = "KAYAK_UI_CAMERA"; + pub fn new() -> Self { + // we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset + // the camera's translation by far and use a right handed coordinate system + let far = 1000.0; + + let orthographic_projection = UIOrthographicProjection { + far, + window_origin: WindowOrigin::BottomLeft, + ..Default::default() + }; + + let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); + + let view_projection = + orthographic_projection.get_projection_matrix() * transform.compute_matrix().inverse(); + let frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + orthographic_projection.far(), + ); + UICameraBundle { + camera: Camera { + priority: isize::MAX - 1, + ..Default::default() + }, + camera_render_graph: CameraRenderGraph::new(crate::render::draw_ui_graph::NAME), + orthographic_projection, + frustum, + visible_entities: VisibleEntities::default(), + transform, + global_transform: Default::default(), + marker: CameraUiKayak, + } + } +} diff --git a/bevy_kayak_renderer/src/camera/mod.rs b/src/camera/mod.rs similarity index 96% rename from bevy_kayak_renderer/src/camera/mod.rs rename to src/camera/mod.rs index 03e5781..b569899 100644 --- a/bevy_kayak_renderer/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -1,23 +1,23 @@ -use bevy::{ - prelude::{CoreStage, Plugin}, - render::{camera::CameraProjectionPlugin, extract_component::ExtractComponentPlugin}, -}; - -mod camera; -mod ortho; - -pub use camera::{CameraUiKayak, UICameraBundle}; -pub(crate) use ortho::UIOrthographicProjection; - -pub struct KayakUICameraPlugin; - -impl Plugin for KayakUICameraPlugin { - fn build(&self, app: &mut bevy::prelude::App) { - app.add_system_to_stage( - CoreStage::PostUpdate, - bevy::render::camera::camera_system::<UIOrthographicProjection>, - ) - .add_plugin(CameraProjectionPlugin::<UIOrthographicProjection>::default()) - .add_plugin(ExtractComponentPlugin::<CameraUiKayak>::default()); - } -} +use bevy::{ + prelude::{CoreStage, Plugin}, + render::{camera::CameraProjectionPlugin, extract_component::ExtractComponentPlugin}, +}; + +mod camera; +mod ortho; + +pub use camera::{CameraUiKayak, UICameraBundle}; +pub(crate) use ortho::UIOrthographicProjection; + +pub struct KayakUICameraPlugin; + +impl Plugin for KayakUICameraPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_system_to_stage( + CoreStage::PostUpdate, + bevy::render::camera::camera_system::<UIOrthographicProjection>, + ) + .add_plugin(CameraProjectionPlugin::<UIOrthographicProjection>::default()) + .add_plugin(ExtractComponentPlugin::<CameraUiKayak>::default()); + } +} diff --git a/bevy_kayak_renderer/src/camera/ortho.rs b/src/camera/ortho.rs similarity index 86% rename from bevy_kayak_renderer/src/camera/ortho.rs rename to src/camera/ortho.rs index d4d07f5..9d8e717 100644 --- a/bevy_kayak_renderer/src/camera/ortho.rs +++ b/src/camera/ortho.rs @@ -1,74 +1,68 @@ -use bevy::ecs::reflect::ReflectComponent; -use bevy::prelude::Component; -use bevy::{ - math::Mat4, - reflect::Reflect, - render::camera::{CameraProjection, DepthCalculation, ScalingMode, WindowOrigin}, -}; - -#[derive(Debug, Clone, Component, Reflect)] -#[reflect(Component)] -pub struct UIOrthographicProjection { - pub left: f32, - pub right: f32, - pub bottom: f32, - pub top: f32, - pub near: f32, - pub far: f32, - pub window_origin: WindowOrigin, - pub scaling_mode: ScalingMode, - pub scale: f32, - pub depth_calculation: DepthCalculation, -} - -impl CameraProjection for UIOrthographicProjection { - fn get_projection_matrix(&self) -> Mat4 { - Mat4::orthographic_rh( - self.left * self.scale, - self.right * self.scale, - self.bottom * self.scale, - self.top * self.scale, - // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] - // This is for interoperability with pipelines using infinite reverse perspective projections. - self.far, - self.near, - ) - } - - fn update(&mut self, width: f32, height: f32) { - match (&self.scaling_mode, &self.window_origin) { - (ScalingMode::WindowSize, WindowOrigin::BottomLeft) => { - self.left = 0.0; - self.right = width; - self.top = 0.0; - self.bottom = height; - } - _ => {} - } - } - - fn depth_calculation(&self) -> DepthCalculation { - self.depth_calculation - } - - fn far(&self) -> f32 { - self.far - } -} - -impl Default for UIOrthographicProjection { - fn default() -> Self { - UIOrthographicProjection { - left: -1.0, - right: 1.0, - bottom: -1.0, - top: 1.0, - near: 0.0, - far: 1000.0, - window_origin: WindowOrigin::Center, - scaling_mode: ScalingMode::WindowSize, - scale: 1.0, - depth_calculation: DepthCalculation::Distance, - } - } -} +use bevy::ecs::reflect::ReflectComponent; +use bevy::prelude::Component; +use bevy::{ + math::Mat4, + reflect::Reflect, + render::camera::{CameraProjection, ScalingMode, WindowOrigin}, +}; + +#[derive(Debug, Clone, Component, Reflect)] +#[reflect(Component)] +pub struct UIOrthographicProjection { + pub left: f32, + pub right: f32, + pub bottom: f32, + pub top: f32, + pub near: f32, + pub far: f32, + pub window_origin: WindowOrigin, + pub scaling_mode: ScalingMode, + pub scale: f32, +} + +impl CameraProjection for UIOrthographicProjection { + fn get_projection_matrix(&self) -> Mat4 { + Mat4::orthographic_rh( + self.left * self.scale, + self.right * self.scale, + self.bottom * self.scale, + self.top * self.scale, + // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] + // This is for interoperability with pipelines using infinite reverse perspective projections. + self.far, + self.near, + ) + } + + fn update(&mut self, width: f32, height: f32) { + match (&self.scaling_mode, &self.window_origin) { + (ScalingMode::WindowSize, WindowOrigin::BottomLeft) => { + self.left = 0.0; + self.right = width; + self.top = 0.0; + self.bottom = height; + } + _ => {} + } + } + + fn far(&self) -> f32 { + self.far + } +} + +impl Default for UIOrthographicProjection { + fn default() -> Self { + UIOrthographicProjection { + left: -1.0, + right: 1.0, + bottom: -1.0, + top: 1.0, + near: 0.0, + far: 1000.0, + window_origin: WindowOrigin::Center, + scaling_mode: ScalingMode::WindowSize, + scale: 1.0, + } + } +} diff --git a/src/children.rs b/src/children.rs new file mode 100644 index 0000000..70962ee --- /dev/null +++ b/src/children.rs @@ -0,0 +1,45 @@ +use bevy::prelude::*; + +use crate::prelude::WidgetContext; + +/// Defers widgets being added to the widget tree. +#[derive(Component, Debug, Default, Clone)] +pub struct KChildren { + inner: Vec<Entity>, +} + +impl KChildren { + pub fn new() -> Self { + Self { inner: Vec::new() } + } + + /// Adds a widget entity to child storage. + pub fn add(&mut self, widget_entity: Entity) { + self.inner.push(widget_entity); + } + + pub fn get(&self, index: usize) -> Option<Entity> { + self.inner.get(index).and_then(|e| Some(*e)) + } + + pub fn remove(&mut self, index: usize) -> Option<Entity> { + if index < self.inner.len() { + Some(self.inner.remove(index)) + } else { + None + } + } + + pub fn despawn(&mut self, commands: &mut Commands) { + for child in self.inner.drain(..) { + commands.entity(child).despawn_recursive(); + } + } + + /// Processes all widgets and adds them to the tree. + pub fn process(&self, widget_context: &WidgetContext, parent_id: Option<Entity>) { + for child in self.inner.iter() { + widget_context.add_widget(parent_id, *child); + } + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..135bd41 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,500 @@ +use std::sync::{Arc, RwLock}; + +use bevy::{ + ecs::{event::ManualEventReader, system::CommandQueue}, + prelude::*, + utils::HashMap, +}; +use morphorm::Hierarchy; + +use crate::{ + calculate_nodes::calculate_nodes, + context_entities::ContextEntities, + event_dispatcher::EventDispatcher, + focus_tree::FocusTree, + layout::{LayoutCache, Rect}, + layout_dispatcher::LayoutEventDispatcher, + node::{DirtyNode, WrappedIndex}, + prelude::WidgetContext, + render_primitive::RenderPrimitive, + tree::{Change, Tree}, + Focusable, WindowSize, +}; + +/// A tag component representing when a widget has been mounted(added to the tree). +#[derive(Component)] +pub struct Mounted; + +const UPDATE_DEPTH: u32 = 0; + +#[derive(Resource)] +pub struct Context { + pub tree: Arc<RwLock<Tree>>, + pub(crate) layout_cache: Arc<RwLock<LayoutCache>>, + pub(crate) focus_tree: Arc<RwLock<FocusTree>>, + systems: HashMap<String, Box<dyn System<In = (WidgetContext, Entity), Out = bool>>>, + pub(crate) current_z: f32, + pub(crate) context_entities: ContextEntities, + pub(crate) current_cursor: CursorIcon, +} + +impl Context { + pub fn new() -> Self { + Self { + tree: Arc::new(RwLock::new(Tree::default())), + layout_cache: Arc::new(RwLock::new(LayoutCache::default())), + focus_tree: Default::default(), + systems: HashMap::default(), + current_z: 0.0, + context_entities: ContextEntities::new(), + current_cursor: CursorIcon::Default, + } + } + + pub(crate) fn get_layout(&self, id: &WrappedIndex) -> Option<Rect> { + if let Ok(cache) = self.layout_cache.try_read() { + cache.rect.get(id).cloned() + } else { + None + } + } + + pub fn add_widget_system<Params>( + &mut self, + type_name: impl Into<String>, + system: impl IntoSystem<(WidgetContext, Entity), bool, Params>, + ) { + let system = IntoSystem::into_system(system); + self.systems.insert(type_name.into(), Box::new(system)); + } + + pub fn add_widget(&mut self, parent: Option<Entity>, entity: Entity) { + if let Ok(mut tree) = self.tree.write() { + tree.add( + WrappedIndex(entity), + parent.and_then(|p| Some(WrappedIndex(p))), + ); + if let Ok(mut cache) = self.layout_cache.try_write() { + cache.add(WrappedIndex(entity)); + } + } + } + + /// Creates a new context using the context entity for the given type_id + parent id. + pub fn set_context_entity<T: Default + 'static>( + &self, + parent_id: Option<Entity>, + context_entity: Entity, + ) { + if let Some(parent_id) = parent_id { + self.context_entities + .add_context_entity::<T>(parent_id, context_entity); + } + } + + pub fn get_child_at(&self, entity: Option<Entity>) -> Option<Entity> { + if let Ok(tree) = self.tree.try_read() { + if let Some(entity) = entity { + let children = tree.child_iter(WrappedIndex(entity)).collect::<Vec<_>>(); + return children.get(0).cloned().map(|index| index.0); + } + } + None + } + + pub fn build_render_primitives( + &self, + nodes: &Query<&crate::node::Node>, + ) -> Vec<RenderPrimitive> { + let node_tree = self.tree.try_read(); + if node_tree.is_err() { + return vec![]; + } + + let node_tree = node_tree.unwrap(); + + if node_tree.root_node.is_none() { + return vec![]; + } + + // self.node_tree.dump(); + + recurse_node_tree_to_build_primitives( + &*node_tree, + &self.layout_cache, + nodes, + node_tree.root_node.unwrap(), + 0.0, + RenderPrimitive::Empty, + ) + } +} + +fn recurse_node_tree_to_build_primitives( + node_tree: &Tree, + layout_cache: &Arc<RwLock<LayoutCache>>, + nodes: &Query<&crate::node::Node>, + current_node: WrappedIndex, + mut main_z_index: f32, + mut prev_clip: RenderPrimitive, +) -> Vec<RenderPrimitive> { + let mut render_primitives = Vec::new(); + if let Ok(node) = nodes.get(current_node.0) { + if let Ok(cache) = layout_cache.try_read() { + if let Some(layout) = cache.rect.get(¤t_node) { + let mut render_primitive = node.primitive.clone(); + let mut layout = *layout; + let new_z_index = if matches!(render_primitive, RenderPrimitive::Clip { .. }) { + main_z_index - 0.1 + } else { + main_z_index + }; + layout.z_index = new_z_index; + render_primitive.set_layout(layout); + render_primitives.push(render_primitive.clone()); + + let new_prev_clip = if matches!(render_primitive, RenderPrimitive::Clip { .. }) { + render_primitive.clone() + } else { + prev_clip + }; + + prev_clip = new_prev_clip.clone(); + if node_tree.children.contains_key(¤t_node) { + for child in node_tree.children.get(¤t_node).unwrap() { + main_z_index += 1.0; + render_primitives.extend(recurse_node_tree_to_build_primitives( + node_tree, + layout_cache, + nodes, + *child, + main_z_index, + new_prev_clip.clone(), + )); + + main_z_index = layout.z_index; + // Between each child node we need to reset the clip. + if matches!(prev_clip, RenderPrimitive::Clip { .. }) { + // main_z_index = new_z_index; + match &mut prev_clip { + RenderPrimitive::Clip { layout } => { + layout.z_index = main_z_index + 0.1; + } + _ => {} + }; + render_primitives.push(prev_clip.clone()); + } + } + } + } else { + println!("No layout for: {:?}", current_node.0.id()); + } + } + } else { + println!("No node for: {:?}", current_node.0.id()); + } + + render_primitives +} + +fn update_widgets_sys(world: &mut World) { + let mut context = world.remove_resource::<Context>().unwrap(); + let tree_iterator = if let Ok(tree) = context.tree.read() { + tree.down_iter().collect::<Vec<_>>() + } else { + panic!("Failed to acquire read lock."); + }; + + // let change_tick = world.increment_change_tick(); + + let old_focus = if let Ok(mut focus_tree) = context.focus_tree.try_write() { + let current = focus_tree.current(); + focus_tree.clear(); + if let Ok(tree) = context.tree.read() { + focus_tree.add(tree.root_node.unwrap(), &tree); + } + current + } else { + None + }; + + let mut new_ticks = HashMap::new(); + + // dbg!("Updating widgets!"); + update_widgets( + world, + &context.tree, + &context.layout_cache, + &mut context.systems, + tree_iterator, + &context.context_entities, + &context.focus_tree, + &mut new_ticks, + ); + + if let Some(old_focus) = old_focus { + if let Ok(mut focus_tree) = context.focus_tree.try_write() { + if focus_tree.contains(old_focus) { + focus_tree.focus(old_focus); + } + } + } + + // dbg!("Finished updating widgets!"); + let tick = world.read_change_tick(); + + for (key, system) in context.systems.iter_mut() { + if let Some(new_tick) = new_ticks.get(key) { + system.set_last_change_tick(*new_tick); + } else { + system.set_last_change_tick(tick); + } + // system.apply_buffers(world); + } + + // if let Ok(tree) = context.tree.try_read() { + // tree.dump(); + // } + + world.insert_resource(context); +} + +fn update_widgets( + world: &mut World, + tree: &Arc<RwLock<Tree>>, + layout_cache: &Arc<RwLock<LayoutCache>>, + systems: &mut HashMap<String, Box<dyn System<In = (WidgetContext, Entity), Out = bool>>>, + widgets: Vec<WrappedIndex>, + context_entities: &ContextEntities, + focus_tree: &Arc<RwLock<FocusTree>>, + new_ticks: &mut HashMap<String, u32>, +) { + for entity in widgets.iter() { + if let Some(entity_ref) = world.get_entity(entity.0) { + if let Some(widget_type) = entity_ref.get::<WidgetName>() { + let widget_context = WidgetContext::new( + tree.clone(), + context_entities.clone(), + layout_cache.clone(), + ); + widget_context.copy_from_point(&tree, *entity); + let children_before = widget_context.get_children(entity.0); + let (widget_context, should_update_children) = update_widget( + systems, + tree, + world, + *entity, + widget_type.0.clone(), + widget_context, + children_before, + new_ticks, + ); + + if should_update_children { + if let Ok(mut tree) = tree.write() { + let diff = tree.diff_children(&widget_context, *entity, UPDATE_DEPTH); + for (_index, child, _parent, changes) in diff.changes.iter() { + for change in changes.iter() { + if matches!(change, Change::Inserted) { + if let Ok(mut cache) = layout_cache.try_write() { + cache.add(*child); + } + } + } + } + // dbg!("Dumping widget tree:"); + // widget_context.dump_at(*entity); + // dbg!(entity.0, &diff); + + tree.merge(&widget_context, *entity, diff, UPDATE_DEPTH); + // dbg!(tree.dump_at(*entity)); + } + } + + // if should_update_children { + let children = widget_context.child_iter(*entity).collect::<Vec<_>>(); + update_widgets( + world, + tree, + layout_cache, + systems, + children, + context_entities, + focus_tree, + new_ticks, + ); + // } + } + } + + if let Some(entity_ref) = world.get_entity(entity.0) { + if entity_ref.contains::<Focusable>() { + if let Ok(tree) = tree.try_read() { + if let Ok(mut focus_tree) = focus_tree.try_write() { + focus_tree.add(*entity, &tree); + } + } + } + } + } +} + +fn update_widget( + systems: &mut HashMap<String, Box<dyn System<In = (WidgetContext, Entity), Out = bool>>>, + tree: &Arc<RwLock<Tree>>, + world: &mut World, + entity: WrappedIndex, + widget_type: String, + widget_context: WidgetContext, + previous_children: Vec<Entity>, + new_ticks: &mut HashMap<String, u32>, +) -> (Tree, bool) { + let should_update_children; + { + // Remove children from previous render. + widget_context.remove_children(previous_children); + let widget_system = systems.get_mut(&widget_type).unwrap(); + let old_tick = widget_system.get_last_change_tick(); + should_update_children = widget_system.run((widget_context.clone(), entity.0), world); + let new_tick = widget_system.get_last_change_tick(); + new_ticks.insert(widget_type.clone(), new_tick); + widget_system.set_last_change_tick(old_tick); + widget_system.apply_buffers(world); + } + let widget_context = widget_context.take(); + let mut command_queue = CommandQueue::default(); + let mut commands = Commands::new(&mut command_queue, world); + + commands.entity(entity.0).remove::<Mounted>(); + + // Mark node as needing a recalculation of rendering/layout. + if should_update_children { + commands.entity(entity.0).insert(DirtyNode); + } + + let diff = if let Ok(tree) = tree.read() { + tree.diff_children(&widget_context, entity, UPDATE_DEPTH) + } else { + panic!("Failed to acquire read lock."); + }; + if should_update_children { + for (_, changed_entity, _, changes) in diff.changes.iter() { + if changes.iter().any(|change| *change != Change::Deleted) { + commands.entity(changed_entity.0).insert(DirtyNode); + } + + if changes.iter().any(|change| *change == Change::Deleted) { + // commands.entity(changed_entity.0).despawn(); + commands.entity(changed_entity.0).remove::<DirtyNode>(); + } + if changes.iter().any(|change| *change == Change::Inserted) { + commands.entity(changed_entity.0).insert(Mounted); + } + } + } + command_queue.apply(world); + + (widget_context, should_update_children) +} + +fn init_systems(world: &mut World) { + let mut context = world.remove_resource::<Context>().unwrap(); + for system in context.systems.values_mut() { + system.initialize(world); + } + + world.insert_resource(context); +} + +pub struct ContextPlugin; + +#[derive(Resource)] +pub struct CustomEventReader<T: bevy::ecs::event::Event>(pub ManualEventReader<T>); + +impl Plugin for ContextPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(WindowSize::default()) + .insert_resource(EventDispatcher::new()) + .insert_resource(CustomEventReader(ManualEventReader::< + bevy::window::CursorMoved, + >::default())) + .insert_resource(CustomEventReader(ManualEventReader::< + bevy::input::mouse::MouseButtonInput, + >::default())) + .insert_resource(CustomEventReader(ManualEventReader::< + bevy::input::mouse::MouseWheel, + >::default())) + .insert_resource(CustomEventReader(ManualEventReader::< + bevy::window::ReceivedCharacter, + >::default())) + .insert_resource(CustomEventReader(ManualEventReader::< + bevy::input::keyboard::KeyboardInput, + >::default())) + .add_plugin(crate::camera::KayakUICameraPlugin) + .add_plugin(crate::render::BevyKayakUIRenderPlugin) + .register_type::<Node>() + .add_startup_system_to_stage(StartupStage::PostStartup, init_systems.at_end()) + .add_system_to_stage(CoreStage::Update, crate::input::process_events) + .add_system_to_stage(CoreStage::PostUpdate, update_widgets_sys.at_start()) + .add_system_to_stage(CoreStage::PostUpdate, calculate_ui.at_end()) + .add_system(crate::window_size::update_window_size); + } +} + +fn calculate_ui(world: &mut World) { + // dbg!("Calculating nodes!"); + let mut system = IntoSystem::into_system(calculate_nodes); + system.initialize(world); + + for _ in 0..5 { + system.run((), world); + system.apply_buffers(world); + world.resource_scope::<Context, _>(|world, mut context| { + LayoutEventDispatcher::dispatch(&mut context, world); + }); + } + + world.resource_scope::<Context, _>(|world, mut context| { + world.resource_scope::<EventDispatcher, _>(|world, event_dispatcher| { + if event_dispatcher.hovered.is_none() { + context.current_cursor = CursorIcon::Default; + return; + } + + let hovered = event_dispatcher.hovered.unwrap(); + if let Some(entity) = world.get_entity(hovered.0) { + if let Some(node) = entity.get::<crate::node::Node>() { + let icon = node.resolved_styles.cursor.resolve(); + context.current_cursor = icon.0; + } + } + }); + + if let Some(ref mut windows) = world.get_resource_mut::<Windows>() { + if let Some(window) = windows.get_primary_mut() { + window.set_cursor_icon(context.current_cursor); + } + } + }); + + + // dbg!("Finished calculating nodes!"); + + // dbg!("Dispatching layout events!"); + // dbg!("Finished dispatching layout events!"); +} + +#[derive(Component, Debug)] +pub struct WidgetName(pub String); + +impl From<String> for WidgetName { + fn from(value: String) -> Self { + WidgetName(value) + } +} + +impl Into<String> for WidgetName { + fn into(self) -> String { + self.0.into() + } +} diff --git a/src/context_entities.rs b/src/context_entities.rs new file mode 100644 index 0000000..a2749a4 --- /dev/null +++ b/src/context_entities.rs @@ -0,0 +1,40 @@ +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; + +use bevy::prelude::Entity; +use dashmap::DashMap; + +#[derive(Debug, Clone)] +pub struct ContextEntities { + ce: Arc<DashMap<Entity, DashMap<TypeId, Entity>>>, +} + +impl ContextEntities { + pub fn new() -> Self { + Self { + ce: Arc::new(DashMap::new()), + } + } + + pub fn add_context_entity<T: Default + 'static>( + &self, + parent_id: Entity, + context_entity: Entity, + ) { + if !self.ce.contains_key(&parent_id) { + self.ce.insert(parent_id, DashMap::new()); + } + let inner = self.ce.get(&parent_id).unwrap(); + inner.insert(T::default().type_id(), context_entity); + } + + pub fn get_context_entity<T: Default + 'static>(&self, parent_id: Entity) -> Option<Entity> { + if !self.ce.contains_key(&parent_id) { + return None; + } + let inner = self.ce.get(&parent_id).unwrap(); + inner.get(&T::default().type_id()).and_then(|e| Some(*e)) + } +} diff --git a/kayak_core/src/cursor.rs b/src/cursor.rs similarity index 96% rename from kayak_core/src/cursor.rs rename to src/cursor.rs index 0ed7dbd..2b45390 100644 --- a/kayak_core/src/cursor.rs +++ b/src/cursor.rs @@ -1,47 +1,47 @@ -/// Controls how the cursor interacts on a given node -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum PointerEvents { - /// Allow all pointer events on this node and its children - All, - /// Allow pointer events on this node but not on its children - SelfOnly, - /// Allow pointer events on this node's children but not on itself - ChildrenOnly, - /// Disallow all pointer events on this node and its children - None, -} - -impl Default for PointerEvents { - fn default() -> Self { - Self::All - } -} - -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub struct CursorEvent { - pub pressed: bool, - pub just_pressed: bool, - pub just_released: bool, - pub position: (f32, f32), -} - -/// An event created on scroll -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub struct ScrollEvent { - /// The amount scrolled - pub delta: ScrollUnit, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum ScrollUnit { - /// A scroll unit that goes by a "line of text" - Line { x: f32, y: f32 }, - /// A scroll unit that goes by individual pixels - Pixel { x: f32, y: f32 }, -} - -impl Default for ScrollUnit { - fn default() -> Self { - ScrollUnit::Pixel { x: 0.0, y: 0.0 } - } -} +/// Controls how the cursor interacts on a given node +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PointerEvents { + /// Allow all pointer events on this node and its children + All, + /// Allow pointer events on this node but not on its children + SelfOnly, + /// Allow pointer events on this node's children but not on itself + ChildrenOnly, + /// Disallow all pointer events on this node and its children + None, +} + +impl Default for PointerEvents { + fn default() -> Self { + Self::All + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq)] +pub struct CursorEvent { + pub pressed: bool, + pub just_pressed: bool, + pub just_released: bool, + pub position: (f32, f32), +} + +/// An event created on scroll +#[derive(Default, Debug, Copy, Clone, PartialEq)] +pub struct ScrollEvent { + /// The amount scrolled + pub delta: ScrollUnit, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ScrollUnit { + /// A scroll unit that goes by a "line of text" + Line { x: f32, y: f32 }, + /// A scroll unit that goes by individual pixels + Pixel { x: f32, y: f32 }, +} + +impl Default for ScrollUnit { + fn default() -> Self { + ScrollUnit::Pixel { x: 0.0, y: 0.0 } + } +} diff --git a/kayak_core/src/event.rs b/src/event.rs similarity index 85% rename from kayak_core/src/event.rs rename to src/event.rs index 1ea8bdc..282c7e4 100644 --- a/kayak_core/src/event.rs +++ b/src/event.rs @@ -1,29 +1,37 @@ -use crate::cursor::{CursorEvent, ScrollEvent}; -use crate::{Index, KeyboardEvent}; +use bevy::prelude::{Entity, World}; + +use crate::{ + cursor::{CursorEvent, ScrollEvent}, + keyboard_event::KeyboardEvent, + prelude::{OnChange, WidgetContext}, +}; /// An event type sent to widgets -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Event { /// The node targeted by this event - pub target: Index, + pub target: Entity, /// The current target of this event - pub current_target: Index, + pub current_target: Entity, /// The type of event pub event_type: EventType, /// Indicates whether this event should propagate or not pub(crate) should_propagate: bool, /// Indicates whether the default action of this event (if any) has been prevented pub(crate) default_prevented: bool, + /// OnChange systems to call afterwards + pub(crate) on_change_systems: Vec<OnChange>, } impl Default for Event { fn default() -> Self { Self { - target: Default::default(), - current_target: Default::default(), + target: Entity::from_raw(0), + current_target: Entity::from_raw(0), event_type: EventType::Click(CursorEvent::default()), should_propagate: true, default_prevented: false, + on_change_systems: Vec::new(), } } } @@ -33,13 +41,14 @@ impl Event { /// /// This is the preferred method for creating an event as it automatically sets up /// propagation and other event metadata in a standardized manner - pub fn new(target: Index, event_type: EventType) -> Self { + pub fn new(target: Entity, event_type: EventType) -> Self { Self { target, current_target: target, event_type, should_propagate: event_type.propagates(), default_prevented: false, + on_change_systems: Vec::new(), } } @@ -62,6 +71,16 @@ impl Event { pub fn prevent_default(&mut self) { self.default_prevented = true; } + + pub fn add_system(&mut self, system: OnChange) { + self.on_change_systems.push(system); + } + + pub(crate) fn run_on_change(&mut self, world: &mut World, widget_context: WidgetContext) { + for system in self.on_change_systems.drain(..) { + system.try_call(self.current_target, world, widget_context.clone()); + } + } } /// The type of event diff --git a/kayak_core/src/event_dispatcher.rs b/src/event_dispatcher.rs similarity index 53% rename from kayak_core/src/event_dispatcher.rs rename to src/event_dispatcher.rs index 2f6e59d..5aa0e41 100644 --- a/kayak_core/src/event_dispatcher.rs +++ b/src/event_dispatcher.rs @@ -1,19 +1,27 @@ -use crate::flo_binding::{Binding, MutableBound}; +use bevy::{ + prelude::{Entity, KeyCode, Resource, World}, + utils::{HashMap, HashSet}, +}; -use crate::cursor::{CursorEvent, ScrollEvent, ScrollUnit}; -use crate::layout_cache::Rect; -use crate::render_command::RenderCommand; -use crate::widget_manager::WidgetManager; use crate::{ - BoxedWidget, Event, EventType, Index, InputEvent, InputEventCategory, KayakContext, - KayakContextRef, KeyCode, KeyboardEvent, KeyboardModifiers, PointerEvents, + context::Context, + cursor::{CursorEvent, PointerEvents, ScrollEvent, ScrollUnit}, + event::{Event, EventType}, + focus_tree::FocusTree, + input_event::{InputEvent, InputEventCategory}, + keyboard_event::{KeyboardEvent, KeyboardModifiers}, + layout::Rect, + node::WrappedIndex, + on_event::OnEvent, + prelude::WidgetContext, + styles::{KStyle, RenderCommand}, + Focusable, }; -use std::collections::{HashMap, HashSet}; -type EventMap = HashMap<Index, HashSet<EventType>>; +type EventMap = HashMap<WrappedIndex, HashSet<EventType>>; type TreeNode = ( // The node ID - Index, + WrappedIndex, // The node depth isize, ); @@ -21,7 +29,7 @@ type TreeNode = ( #[derive(Debug, Clone)] struct EventState { best_z_index: f32, - best_match: Option<Index>, + best_match: Option<WrappedIndex>, best_depth: isize, } @@ -35,7 +43,7 @@ impl Default for EventState { } } -#[derive(Debug, Clone)] +#[derive(Resource, Debug, Clone)] pub(crate) struct EventDispatcher { is_mouse_pressed: bool, next_mouse_pressed: bool, @@ -43,18 +51,18 @@ pub(crate) struct EventDispatcher { next_mouse_position: (f32, f32), previous_events: EventMap, keyboard_modifiers: KeyboardModifiers, - pub last_clicked: Binding<Index>, + // pub last_clicked: Binding<WrappedIndex>, contains_cursor: Option<bool>, wants_cursor: Option<bool>, - has_cursor: Option<Index>, - pub cursor_capture: Option<Index>, - pub hovered: Option<Index>, + has_cursor: Option<WrappedIndex>, + pub cursor_capture: Option<WrappedIndex>, + pub hovered: Option<WrappedIndex>, } impl EventDispatcher { pub fn new() -> Self { Self { - last_clicked: Binding::new(Index::default()), + // last_clicked: Binding::new(WrappedIndex(Entity::from_raw(0))), is_mouse_pressed: Default::default(), next_mouse_pressed: Default::default(), current_mouse_position: Default::default(), @@ -82,9 +90,9 @@ impl EventDispatcher { } /// Captures all cursor events and instead makes the given index the target - pub fn capture_cursor(&mut self, index: Index) -> Option<Index> { + pub fn capture_cursor(&mut self, index: Entity) -> Option<WrappedIndex> { let old = self.cursor_capture; - self.cursor_capture = Some(index); + self.cursor_capture = Some(WrappedIndex(index)); old } @@ -97,8 +105,8 @@ impl EventDispatcher { /// /// This check can be side-stepped if necessary by calling [`force_release_cursor`](Self::force_release_cursor) /// instead (or by calling this method with the correct index). - pub fn release_cursor(&mut self, index: Index) -> bool { - if self.cursor_capture == Some(index) { + pub fn release_cursor(&mut self, index: Entity) -> bool { + if self.cursor_capture == Some(WrappedIndex(index)) { self.force_release_cursor(); true } else { @@ -112,7 +120,7 @@ impl EventDispatcher { /// /// This will force the release, regardless of which widget has called it. To safely release, /// use the standard [`release_cursor`](Self::release_cursor) method instead. - pub fn force_release_cursor(&mut self) -> Option<Index> { + pub fn force_release_cursor(&mut self) -> Option<WrappedIndex> { let old = self.cursor_capture; self.cursor_capture = None; old @@ -148,65 +156,103 @@ impl EventDispatcher { /// The currently hovered node #[allow(dead_code)] - pub fn hovered(&self) -> Option<Index> { + pub fn hovered(&self) -> Option<WrappedIndex> { self.hovered } /// Process and dispatch an [InputEvent](crate::InputEvent) - #[allow(dead_code)] - pub fn process_event(&mut self, input_event: InputEvent, context: &mut KayakContext) { - let events = self.build_event_stream(&[input_event], &mut context.widget_manager); - self.dispatch_events(events, context); - } + // #[allow(dead_code)] + // pub fn process_event( + // &mut self, + // input_event: InputEvent, + // context: &mut Context, + // focusable_query: &Query<&Focusable>, + // style_query: &Query<&Style>, + // on_event_query: &Query<&OnEvent>, + // ) { + // let events = self.build_event_stream(&[input_event], context, focusable_query, style_query); + // self.dispatch_events(events, context, on_event_query); + // } /// Process and dispatch a set of [InputEvents](crate::InputEvent) - pub fn process_events(&mut self, input_events: Vec<InputEvent>, context: &mut KayakContext) { - let events = self.build_event_stream(&input_events, &mut context.widget_manager); - self.dispatch_events(events, context); + pub fn process_events( + &mut self, + input_events: Vec<InputEvent>, + context: &mut Context, + world: &mut World, + ) { + let events = { self.build_event_stream(&input_events, context, world) }; + self.dispatch_events(events, context, world); } /// Dispatch an [Event](crate::Event) #[allow(dead_code)] - pub fn dispatch_event(&mut self, event: Event, context: &mut KayakContext) { - self.dispatch_events(vec![event], context); + pub fn dispatch_event(&mut self, event: Event, context: &mut Context, world: &mut World) { + self.dispatch_events(vec![event], context, world); } /// Dispatch a set of [Events](crate::Event) - pub fn dispatch_events(&mut self, events: Vec<Event>, context: &mut KayakContext) { + pub fn dispatch_events( + &mut self, + events: Vec<Event>, + context: &mut Context, + world: &mut World, + ) { // === Dispatch Events === // let mut next_events = HashMap::default(); for mut event in events { - let mut current_target: Option<Index> = Some(event.target); + let mut current_target: Option<WrappedIndex> = Some(WrappedIndex(event.target)); while let Some(index) = current_target { // Create a copy of the event, specific for this node // This is to make sure unauthorized changes to the event are not propagated // (e.g., changing the event type, removing the target, etc.) let mut node_event = Event { - current_target: index, - ..event + current_target: index.0, + ..event.clone() }; // --- Update State --- // Self::insert_event(&mut next_events, &index, node_event.event_type); // --- Call Event --- // - let mut target_widget = context.widget_manager.take(index); - let mut ctx = KayakContextRef::new(context, Some(index)); - target_widget.on_event(&mut ctx, &mut node_event); - context.widget_manager.repossess(target_widget); + if let Some(mut entity) = world.get_entity_mut(index.0) { + if let Some(mut on_event) = entity.remove::<OnEvent>() { + let mut event_dispatcher_context = EventDispatcherContext { + cursor_capture: self.cursor_capture, + }; + + (event_dispatcher_context, node_event) = + on_event.try_call(event_dispatcher_context, index.0, node_event, world); + world.entity_mut(index.0).insert(on_event); + event_dispatcher_context.merge(self); + + // Sometimes events will require systems to be called. + // IE OnChange + let widget_context = WidgetContext::new( + context.tree.clone(), + context.context_entities.clone(), + context.layout_cache.clone(), + ); + node_event.run_on_change(world, widget_context); + } + } event.default_prevented |= node_event.default_prevented; // --- Propagate Event --- // if node_event.should_propagate { - current_target = context.widget_manager.node_tree.get_parent(index); + if let Ok(node_tree) = context.tree.try_read() { + current_target = node_tree.get_parent(index); + } else { + current_target = None; + } } else { current_target = None; } } if !event.default_prevented { - self.execute_default(event, context); + self.execute_default(event, context, world); } } @@ -248,175 +294,184 @@ impl EventDispatcher { fn build_event_stream( &mut self, input_events: &[InputEvent], - widget_manager: &mut WidgetManager, + context: &mut Context, + world: &mut World, ) -> Vec<Event> { let mut event_stream = Vec::<Event>::new(); let mut states: HashMap<EventType, EventState> = HashMap::new(); - let root = if let Some(root) = widget_manager.node_tree.root_node { - root - } else { - return event_stream; - }; - - // === Setup Cursor States === // - let old_hovered = self.hovered; - let old_contains_cursor = self.contains_cursor; - let old_wants_cursor = self.wants_cursor; - self.hovered = None; - self.contains_cursor = None; - self.wants_cursor = None; - self.next_mouse_position = self.current_mouse_position; - self.next_mouse_pressed = self.is_mouse_pressed; - - // --- Pre-Process --- // - // We pre-process some events so that we can provide accurate event data (such as if the mouse is pressed) - // This is faster than resolving data after the fact since `input_events` is generally very small - for input_event in input_events { - if let InputEvent::MouseMoved(point) = input_event { - // Reset next global mouse position - self.next_mouse_position = *point; - } - - if matches!(input_event, InputEvent::MouseLeftPress) { - // Reset next global mouse pressed - self.next_mouse_pressed = true; - break; - } else if matches!(input_event, InputEvent::MouseLeftRelease) { - // Reset next global mouse pressed - self.next_mouse_pressed = false; - // Reset global cursor container - self.has_cursor = None; - break; - } - } - - // === Mouse Events === // - if let Some(captor) = self.cursor_capture { - // A widget has been set to capture pointer events -> it should be the only one receiving events + if let Ok(node_tree) = context.tree.try_read() { + let root = if let Some(root) = node_tree.root_node { + root + } else { + return event_stream; + }; + + // === Setup Cursor States === // + let old_hovered = self.hovered; + let old_contains_cursor = self.contains_cursor; + let old_wants_cursor = self.wants_cursor; + self.hovered = None; + self.contains_cursor = None; + self.wants_cursor = None; + self.next_mouse_position = self.current_mouse_position; + self.next_mouse_pressed = self.is_mouse_pressed; + + // --- Pre-Process --- // + // We pre-process some events so that we can provide accurate event data (such as if the mouse is pressed) + // This is faster than resolving data after the fact since `input_events` is generally very small for input_event in input_events { - // --- Process Event --- // - if matches!(input_event.category(), InputEventCategory::Mouse) { - // A widget's PointerEvents style will determine how it and its children are processed - let pointer_events = Self::resolve_pointer_events(captor, widget_manager); - - match pointer_events { - PointerEvents::All | PointerEvents::SelfOnly => { - let events = self.process_pointer_events( - input_event, - (captor, 0), - &mut states, - widget_manager, - true, - ); - event_stream.extend(events); - } - _ => {} - } + if let InputEvent::MouseMoved(point) = input_event { + // Reset next global mouse position + self.next_mouse_position = *point; + } + + if matches!(input_event, InputEvent::MouseLeftPress) { + // Reset next global mouse pressed + self.next_mouse_pressed = true; + break; + } else if matches!(input_event, InputEvent::MouseLeftRelease) { + // Reset next global mouse pressed + self.next_mouse_pressed = false; + // Reset global cursor container + self.has_cursor = None; + break; } } - } else { - // No capturing widget -> process cursor events as normal - let mut stack: Vec<TreeNode> = vec![(root, 0)]; - while stack.len() > 0 { - let (current, depth) = stack.pop().unwrap(); - let mut enter_children = true; + // === Mouse Events === // + if let Some(captor) = self.cursor_capture { + // A widget has been set to capture pointer events -> it should be the only one receiving events for input_event in input_events { // --- Process Event --- // if matches!(input_event.category(), InputEventCategory::Mouse) { // A widget's PointerEvents style will determine how it and its children are processed - let pointer_events = Self::resolve_pointer_events(current, widget_manager); + let pointer_events = Self::resolve_pointer_events(captor, world); match pointer_events { PointerEvents::All | PointerEvents::SelfOnly => { let events = self.process_pointer_events( input_event, - (current, depth), + (captor, 0), &mut states, - widget_manager, - false, + world, + context, + true, ); event_stream.extend(events); - - if matches!(pointer_events, PointerEvents::SelfOnly) { - enter_children = false; - } } - PointerEvents::None => enter_children = false, - PointerEvents::ChildrenOnly => {} + _ => {} } } } + } else { + // No capturing widget -> process cursor events as normal + let mut stack: Vec<TreeNode> = vec![(root, 0)]; + while stack.len() > 0 { + let (current, depth) = stack.pop().unwrap(); + let mut enter_children = true; + + for input_event in input_events { + // --- Process Event --- // + if matches!(input_event.category(), InputEventCategory::Mouse) { + // A widget's PointerEvents style will determine how it and its children are processed + let pointer_events = Self::resolve_pointer_events(current, world); + + match pointer_events { + PointerEvents::All | PointerEvents::SelfOnly => { + let events = self.process_pointer_events( + input_event, + (current, depth), + &mut states, + world, + context, + false, + ); + event_stream.extend(events); + + if matches!(pointer_events, PointerEvents::SelfOnly) { + enter_children = false; + } + } + PointerEvents::None => enter_children = false, + PointerEvents::ChildrenOnly => {} + } + } + } - // --- Push Children to Stack --- // - if enter_children { - if let Some(children) = widget_manager.node_tree.children.get(¤t) { - for child in children { - stack.push((*child, depth + 1)); + // --- Push Children to Stack --- // + if enter_children { + if let Some(children) = node_tree.children.get(¤t) { + for child in children { + stack.push((*child, depth + 1)); + } } } } } - } - - // === Keyboard Events === // - for input_event in input_events { - // Keyboard events only care about the currently focused widget so we don't need to run this over every node in the tree - let events = self.process_keyboard_events(input_event, &mut states, widget_manager); - event_stream.extend(events); - } - // === Additional Events === // - let mut had_focus_event = false; - - // These events are ones that require a specific target and need the tree to be evaluated before selecting the best match - for (event_type, state) in states { - if let Some(node) = state.best_match { - event_stream.push(Event::new(node, event_type)); + if let Ok(mut focus_tree) = context.focus_tree.try_write() { + // === Keyboard Events === // + for input_event in input_events { + // Keyboard events only care about the currently focused widget so we don't need to run this over every node in the tree + let events = + self.process_keyboard_events(input_event, &mut states, &focus_tree); + event_stream.extend(events); + } - match event_type { - EventType::Focus => { - had_focus_event = true; - if let Some(current_focus) = widget_manager.focus_tree.current() { - if current_focus != node { - event_stream.push(Event::new(current_focus, EventType::Blur)); + // === Additional Events === // + let mut had_focus_event = false; + + // These events are ones that require a specific target and need the tree to be evaluated before selecting the best match + for (event_type, state) in states { + if let Some(node) = state.best_match { + event_stream.push(Event::new(node.0, event_type)); + + match event_type { + EventType::Focus => { + had_focus_event = true; + if let Some(current_focus) = focus_tree.current() { + if current_focus != node { + event_stream + .push(Event::new(current_focus.0, EventType::Blur)); + } + } + focus_tree.focus(node); + } + EventType::Hover(..) => { + self.hovered = Some(node); } + _ => {} } - widget_manager.focus_tree.focus(node); - } - EventType::Hover(..) => { - self.hovered = Some(node); } - _ => {} } - } - } - // --- Blur Event --- // - if !had_focus_event && input_events.contains(&InputEvent::MouseLeftPress) { - // A mouse press didn't contain a focus event -> blur - if let Some(current_focus) = widget_manager.focus_tree.current() { - event_stream.push(Event::new(current_focus, EventType::Blur)); - widget_manager.focus_tree.blur(); - } - } + // --- Blur Event --- // + if !had_focus_event && input_events.contains(&InputEvent::MouseLeftPress) { + // A mouse press didn't contain a focus event -> blur + if let Some(current_focus) = focus_tree.current() { + event_stream.push(Event::new(current_focus.0, EventType::Blur)); + focus_tree.blur(); + } + } - // === Process Cursor States === // - self.current_mouse_position = self.next_mouse_position; - self.is_mouse_pressed = self.next_mouse_pressed; + // === Process Cursor States === // + self.current_mouse_position = self.next_mouse_position; + self.is_mouse_pressed = self.next_mouse_pressed; - if self.hovered.is_none() { - // No change -> revert - self.hovered = old_hovered; - } - if self.contains_cursor.is_none() { - // No change -> revert - self.contains_cursor = old_contains_cursor; - } - if self.wants_cursor.is_none() { - // No change -> revert - self.wants_cursor = old_wants_cursor; + if self.hovered.is_none() { + // No change -> revert + self.hovered = old_hovered; + } + if self.contains_cursor.is_none() { + // No change -> revert + self.contains_cursor = old_contains_cursor; + } + if self.wants_cursor.is_none() { + // No change -> revert + self.wants_cursor = old_wants_cursor; + } + } } event_stream @@ -438,7 +493,8 @@ impl EventDispatcher { input_event: &InputEvent, tree_node: TreeNode, states: &mut HashMap<EventType, EventState>, - widget_manager: &WidgetManager, + world: &mut World, + context: &Context, ignore_layout: bool, ) -> Vec<Event> { let mut event_stream = Vec::<Event>::new(); @@ -446,30 +502,31 @@ impl EventDispatcher { match input_event { InputEvent::MouseMoved(point) => { - if let Some(layout) = widget_manager.get_layout(&node) { + if let Some(layout) = context.get_layout(&node) { let cursor_event = self.get_cursor_event(*point); let was_contained = layout.contains(&self.current_mouse_position); let is_contained = layout.contains(point); if !ignore_layout && was_contained != is_contained { if was_contained { - event_stream.push(Event::new(node, EventType::MouseOut(cursor_event))); + event_stream + .push(Event::new(node.0, EventType::MouseOut(cursor_event))); } else { - event_stream.push(Event::new(node, EventType::MouseIn(cursor_event))); + event_stream.push(Event::new(node.0, EventType::MouseIn(cursor_event))); } } if self.contains_cursor.is_none() || !self.contains_cursor.unwrap_or_default() { - if let Some(widget) = widget_manager.current_widgets.get(node).unwrap() { + if let Some(styles) = world.get::<KStyle>(node.0) { // Check if the cursor moved onto a widget that qualifies as one that can contain it - if ignore_layout || Self::can_contain_cursor(widget) { + if ignore_layout || Self::can_contain_cursor(styles) { self.contains_cursor = Some(is_contained); } } } if self.wants_cursor.is_none() || !self.wants_cursor.unwrap_or_default() { - let focusable = widget_manager.get_focusable(node); + let focusable = world.get::<Focusable>(node.0).is_some(); // Check if the cursor moved onto a focusable widget (i.e. one that would want it) - if matches!(focusable, Some(true)) { + if focusable { self.wants_cursor = Some(is_contained); } } @@ -479,29 +536,26 @@ impl EventDispatcher { Self::update_state( states, (node, depth), - layout, + &layout, EventType::Hover(cursor_event), ); } } } InputEvent::MouseLeftPress => { - if let Some(layout) = widget_manager.get_layout(&node) { + if let Some(layout) = context.get_layout(&node) { if ignore_layout || layout.contains(&self.current_mouse_position) { let cursor_event = self.get_cursor_event(self.current_mouse_position); - event_stream.push(Event::new(node, EventType::MouseDown(cursor_event))); + event_stream.push(Event::new(node.0, EventType::MouseDown(cursor_event))); - if let Some(focusable) = widget_manager.get_focusable(node) { - if focusable { - Self::update_state(states, (node, depth), layout, EventType::Focus); - } + if world.get::<Focusable>(node.0).is_some() { + Self::update_state(states, (node, depth), &layout, EventType::Focus); } if self.has_cursor.is_none() { - let widget = widget_manager.current_widgets.get(node).unwrap(); - if let Some(widget) = widget { + if let Some(styles) = world.get::<KStyle>(node.0) { // Check if the cursor moved onto a widget that qualifies as one that can contain it - if Self::can_contain_cursor(widget) { + if Self::can_contain_cursor(styles) { self.has_cursor = Some(node); } } @@ -510,11 +564,11 @@ impl EventDispatcher { } } InputEvent::MouseLeftRelease => { - if let Some(layout) = widget_manager.get_layout(&node) { + if let Some(layout) = context.get_layout(&node) { if ignore_layout || layout.contains(&self.current_mouse_position) { let cursor_event = self.get_cursor_event(self.current_mouse_position); - event_stream.push(Event::new(node, EventType::MouseUp(cursor_event))); - self.last_clicked.set(node); + event_stream.push(Event::new(node.0, EventType::MouseUp(cursor_event))); + // self.last_clicked.set(node); if Self::contains_event( &self.previous_events, @@ -524,7 +578,7 @@ impl EventDispatcher { Self::update_state( states, (node, depth), - layout, + &layout, EventType::Click(cursor_event), ); } @@ -532,13 +586,13 @@ impl EventDispatcher { } } InputEvent::Scroll { dx, dy, is_line } => { - if let Some(layout) = widget_manager.get_layout(&node) { + if let Some(layout) = context.get_layout(&node) { // Check for scroll eligibility if ignore_layout || layout.contains(&self.current_mouse_position) { Self::update_state( states, (node, depth), - layout, + &layout, EventType::Scroll(ScrollEvent { delta: if *is_line { ScrollUnit::Line { x: *dx, y: *dy } @@ -556,12 +610,10 @@ impl EventDispatcher { event_stream } - fn resolve_pointer_events(index: Index, widget_manager: &WidgetManager) -> PointerEvents { + fn resolve_pointer_events(index: WrappedIndex, world: &mut World) -> PointerEvents { let mut pointer_events = PointerEvents::default(); - if let Some(widget) = widget_manager.current_widgets.get(index).unwrap() { - if let Some(styles) = widget.get_props().get_styles() { - pointer_events = styles.pointer_events.resolve(); - } + if let Some(styles) = world.get::<KStyle>(index.0) { + pointer_events = styles.pointer_events.resolve(); } pointer_events } @@ -581,13 +633,13 @@ impl EventDispatcher { &mut self, input_event: &InputEvent, _states: &mut HashMap<EventType, EventState>, - widget_manager: &WidgetManager, + focus_tree: &FocusTree, ) -> Vec<Event> { let mut event_stream = Vec::new(); - if let Some(current_focus) = widget_manager.focus_tree.current() { + if let Some(current_focus) = focus_tree.current() { match input_event { InputEvent::CharEvent { c } => { - event_stream.push(Event::new(current_focus, EventType::CharInput { c: *c })) + event_stream.push(Event::new(current_focus.0, EventType::CharInput { c: *c })) } InputEvent::Keyboard { key, is_pressed } => { // === Modifers === // @@ -610,12 +662,12 @@ impl EventDispatcher { // === Event === // if *is_pressed { event_stream.push(Event::new( - current_focus, + current_focus.0, EventType::KeyDown(KeyboardEvent::new(*key, self.keyboard_modifiers)), )) } else { event_stream.push(Event::new( - current_focus, + current_focus.0, EventType::KeyUp(KeyboardEvent::new(*key, self.keyboard_modifiers)), )) } @@ -650,7 +702,7 @@ impl EventDispatcher { } /// Checks if the given event map contains a specific event for the given widget - fn contains_event(events: &EventMap, widget_id: &Index, event_type: &EventType) -> bool { + fn contains_event(events: &EventMap, widget_id: &WrappedIndex, event_type: &EventType) -> bool { if let Some(entry) = events.get(widget_id) { entry.contains(event_type) } else { @@ -659,7 +711,11 @@ impl EventDispatcher { } /// Insert an event for a widget in the given event map - fn insert_event(events: &mut EventMap, widget_id: &Index, event_type: EventType) -> bool { + fn insert_event( + events: &mut EventMap, + widget_id: &WrappedIndex, + event_type: EventType, + ) -> bool { let entry = events.entry(*widget_id).or_insert(HashSet::default()); entry.insert(event_type) } @@ -668,40 +724,44 @@ impl EventDispatcher { /// /// Currently a valid widget is defined as one where: /// * RenderCommands is neither `Empty` nor `Layout` nor `Clip` - fn can_contain_cursor(widget: &BoxedWidget) -> bool { - if let Some(styles) = widget.get_props().get_styles() { - let cmds = styles.render_command.resolve(); - !matches!( - cmds, - RenderCommand::Empty | RenderCommand::Layout | RenderCommand::Clip - ) - } else { - false - } + fn can_contain_cursor(widget_styles: &KStyle) -> bool { + let cmds = widget_styles.render_command.resolve(); + !matches!( + cmds, + RenderCommand::Empty | RenderCommand::Layout | RenderCommand::Clip + ) } /// Executes default actions for events - fn execute_default(&mut self, event: Event, context: &mut KayakContext) { + fn execute_default(&mut self, event: Event, context: &mut Context, world: &mut World) { match event.event_type { EventType::KeyDown(evt) => match evt.key() { KeyCode::Tab => { - let current_focus = context.widget_manager.focus_tree.current(); - - let index = if evt.is_shift_pressed() { - context.widget_manager.focus_tree.prev() - } else { - context.widget_manager.focus_tree.next() - }; + let (index, current_focus) = + if let Ok(mut focus_tree) = context.focus_tree.try_write() { + let current_focus = focus_tree.current(); + + let index = if evt.is_shift_pressed() { + focus_tree.prev() + } else { + focus_tree.next() + }; + (index, current_focus) + } else { + (None, None) + }; if let Some(index) = index { - let mut events = vec![Event::new(index, EventType::Focus)]; + let mut events = vec![Event::new(index.0, EventType::Focus)]; if let Some(current_focus) = current_focus { if current_focus != index { - events.push(Event::new(current_focus, EventType::Blur)); + events.push(Event::new(current_focus.0, EventType::Blur)); } } - context.widget_manager.focus_tree.focus(index); - self.dispatch_events(events, context); + if let Ok(mut focus_tree) = context.focus_tree.try_write() { + focus_tree.focus(index); + } + self.dispatch_events(events, context, world); } } _ => {} @@ -712,9 +772,9 @@ impl EventDispatcher { /// Merge this `EventDispatcher` with another, taking only the internally mutated data. /// - /// This is meant to solve the issue in `KayakContext`, where [`EventDispatcher::process_events`] and - /// similar methods require mutable access to `KayakContext`, forcing `EventDispatcher` to be cloned - /// before running the method. However, some data mutated through `KayakContext` may be lost when + /// This is meant to solve the issue in `Context`, where [`EventDispatcher::process_events`] and + /// similar methods require mutable access to `Context`, forcing `EventDispatcher` to be cloned + /// before running the method. However, some data mutated through `Context` may be lost when /// re-claiming the `EventDispatcher`. This method ensures that data mutated in such a way will not be /// overwritten during the merge. /// @@ -724,9 +784,9 @@ impl EventDispatcher { /// /// returns: () pub fn merge(&mut self, from: EventDispatcher) { - // Merge only what could be changed internally. External changes (i.e. from KayakContext) + // Merge only what could be changed internally. External changes (i.e. from Context) // should not be touched - self.last_clicked = from.last_clicked; + // self.last_clicked = from.last_clicked; self.is_mouse_pressed = from.is_mouse_pressed; self.next_mouse_pressed = from.next_mouse_pressed; self.current_mouse_position = from.current_mouse_position; @@ -742,3 +802,50 @@ impl EventDispatcher { // self.cursor_capture = from.cursor_capture; } } + +pub struct EventDispatcherContext { + cursor_capture: Option<WrappedIndex>, +} + +impl EventDispatcherContext { + /// Captures all cursor events and instead makes the given index the target + pub fn capture_cursor(&mut self, index: Entity) -> Option<WrappedIndex> { + let old = self.cursor_capture; + self.cursor_capture = Some(WrappedIndex(index)); + old + } + + /// Releases the captured cursor + /// + /// Returns true if successful. + /// + /// This will only release the cursor if the given index matches the current captor. This + /// prevents other widgets from accidentally releasing against the will of the original captor. + /// + /// This check can be side-stepped if necessary by calling [`force_release_cursor`](Self::force_release_cursor) + /// instead (or by calling this method with the correct index). + pub fn release_cursor(&mut self, index: Entity) -> bool { + if self.cursor_capture == Some(WrappedIndex(index)) { + self.force_release_cursor(); + true + } else { + false + } + } + + /// Releases the captured cursor + /// + /// Returns the index of the previous captor. + /// + /// This will force the release, regardless of which widget has called it. To safely release, + /// use the standard [`release_cursor`](Self::release_cursor) method instead. + pub fn force_release_cursor(&mut self) -> Option<WrappedIndex> { + let old = self.cursor_capture; + self.cursor_capture = None; + old + } + + pub(crate) fn merge(self, event_dispatcher: &mut EventDispatcher) { + event_dispatcher.cursor_capture = self.cursor_capture; + } +} diff --git a/kayak_core/src/focus_tree.rs b/src/focus_tree.rs similarity index 81% rename from kayak_core/src/focus_tree.rs rename to src/focus_tree.rs index 320d571..f20fb6b 100644 --- a/kayak_core/src/focus_tree.rs +++ b/src/focus_tree.rs @@ -1,10 +1,14 @@ -use crate::{Index, Tree}; -use std::collections::HashMap; +use bevy::{prelude::Component, utils::HashMap}; + +use crate::{node::WrappedIndex, prelude::Tree}; + +#[derive(Component, Default, Clone, Copy)] +pub struct Focusable; #[derive(Debug, Default, PartialEq)] pub struct FocusTree { tree: Tree, - current_focus: Option<Index>, + current_focus: Option<WrappedIndex>, } /// A struct used to track and calculate widget focusability, based on the following rule: @@ -13,14 +17,14 @@ pub struct FocusTree { #[derive(Debug, Default)] pub(crate) struct FocusTracker { /// The focusability as set by the parent (i.e. from its props) - parents: HashMap<Index, bool>, + parents: HashMap<WrappedIndex, bool>, /// The focusability as set by the widget itself (i.e. from its render function) - widgets: HashMap<Index, bool>, + widgets: HashMap<WrappedIndex, bool>, } impl FocusTree { /// Add the given focusable index to the tree - pub fn add(&mut self, index: Index, widget_tree: &Tree) { + pub fn add(&mut self, index: WrappedIndex, widget_context: &Tree) { // Cases to handle: // 1. Tree empty -> insert root node // 2. Tree not empty @@ -28,7 +32,7 @@ impl FocusTree { // b. Not contains parent -> demote and replace root node let mut current_index = index; - while let Some(parent) = widget_tree.get_parent(current_index) { + while let Some(parent) = widget_context.get_parent(current_index) { current_index = parent; if self.contains(parent) { self.tree.add(index, Some(parent)); @@ -44,7 +48,7 @@ impl FocusTree { } /// Remove the given focusable index from the tree - pub fn remove(&mut self, index: Index) { + pub fn remove(&mut self, index: WrappedIndex) { if self.current_focus == Some(index) { self.blur(); } @@ -57,7 +61,7 @@ impl FocusTree { } /// Checks if the given index is present in the tree - pub fn contains(&self, index: Index) -> bool { + pub fn contains(&self, index: WrappedIndex) -> bool { self.tree.contains(index) } @@ -68,7 +72,7 @@ impl FocusTree { } /// Set the current focus - pub fn focus(&mut self, index: Index) { + pub fn focus(&mut self, index: WrappedIndex) { self.current_focus = Some(index); } @@ -80,24 +84,24 @@ impl FocusTree { } /// Get the currently focused index - pub fn current(&self) -> Option<Index> { + pub fn current(&self) -> Option<WrappedIndex> { self.current_focus } /// Change focus to the next focusable index - pub fn next(&mut self) -> Option<Index> { + pub fn next(&mut self) -> Option<WrappedIndex> { self.current_focus = self.peek_next(); self.current_focus } /// Change focus to the previous focusable index - pub fn prev(&mut self) -> Option<Index> { + pub fn prev(&mut self) -> Option<WrappedIndex> { self.current_focus = self.peek_prev(); self.current_focus } /// Peek the next focusable index without actually changing focus - pub fn peek_next(&self) -> Option<Index> { + pub fn peek_next(&self) -> Option<WrappedIndex> { if let Some(index) = self.current_focus { // === Enter Children === // if let Some(child) = self.tree.get_first_child(index) { @@ -124,7 +128,7 @@ impl FocusTree { } /// Peek the previous focusable index without actually changing focus - pub fn peek_prev(&self) -> Option<Index> { + pub fn peek_prev(&self) -> Option<WrappedIndex> { if let Some(index) = self.current_focus { // === Enter Siblings === // if let Some(sibling) = self.tree.get_prev_sibling(index) { @@ -177,7 +181,7 @@ impl FocusTracker { /// returns: () pub fn set_focusability( &mut self, - index: Index, + index: WrappedIndex, focusable: Option<bool>, is_parent_defined: bool, ) { @@ -201,7 +205,7 @@ impl FocusTracker { /// * `index`: The widget ID /// /// returns: Option<bool> - pub fn get_focusability(&self, index: Index) -> Option<bool> { + pub fn get_focusability(&self, index: WrappedIndex) -> Option<bool> { if let Some(focusable) = self.widgets.get(&index) { Some(*focusable) } else if let Some(focusable) = self.parents.get(&index) { @@ -215,26 +219,28 @@ impl FocusTracker { #[cfg(test)] mod tests { use crate::focus_tree::FocusTree; - use crate::{Index, Tree}; + use crate::node::WrappedIndex; + use crate::tree::Tree; + use bevy::prelude::Entity; #[test] fn next_should_cycle() { let mut focus_tree = FocusTree::default(); let mut tree = Tree::default(); - let a = Index::from_raw_parts(0, 0); + let a = WrappedIndex(Entity::from_raw(0)); tree.add(a, None); - let a_a = Index::from_raw_parts(1, 0); + let a_a = WrappedIndex(Entity::from_raw(1)); tree.add(a_a, Some(a)); - let a_b = Index::from_raw_parts(2, 0); + let a_b = WrappedIndex(Entity::from_raw(2)); tree.add(a_b, Some(a)); - let a_a_a = Index::from_raw_parts(3, 0); + let a_a_a = WrappedIndex(Entity::from_raw(3)); tree.add(a_a_a, Some(a_a)); - let a_a_a_a = Index::from_raw_parts(4, 0); + let a_a_a_a = WrappedIndex(Entity::from_raw(4)); tree.add(a_a_a_a, Some(a_a_a)); - let a_a_a_b = Index::from_raw_parts(5, 0); + let a_a_a_b = WrappedIndex(Entity::from_raw(5)); tree.add(a_a_a_b, Some(a_a_a)); - let a_b_a = Index::from_raw_parts(6, 0); + let a_b_a = WrappedIndex(Entity::from_raw(6)); tree.add(a_b_a, Some(a_b)); focus_tree.add(a, &tree); @@ -264,19 +270,19 @@ mod tests { let mut focus_tree = FocusTree::default(); let mut tree = Tree::default(); - let a = Index::from_raw_parts(0, 0); + let a = WrappedIndex(Entity::from_raw(0)); tree.add(a, None); - let a_a = Index::from_raw_parts(1, 0); + let a_a = WrappedIndex(Entity::from_raw(1)); tree.add(a_a, Some(a)); - let a_b = Index::from_raw_parts(2, 0); + let a_b = WrappedIndex(Entity::from_raw(2)); tree.add(a_b, Some(a)); - let a_a_a = Index::from_raw_parts(3, 0); + let a_a_a = WrappedIndex(Entity::from_raw(3)); tree.add(a_a_a, Some(a_a)); - let a_a_a_a = Index::from_raw_parts(4, 0); + let a_a_a_a = WrappedIndex(Entity::from_raw(4)); tree.add(a_a_a_a, Some(a_a_a)); - let a_a_a_b = Index::from_raw_parts(5, 0); + let a_a_a_b = WrappedIndex(Entity::from_raw(5)); tree.add(a_a_a_b, Some(a_a_a)); - let a_b_a = Index::from_raw_parts(6, 0); + let a_b_a = WrappedIndex(Entity::from_raw(6)); tree.add(a_b_a, Some(a_b)); focus_tree.add(a, &tree); diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..f813f05 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,127 @@ +use bevy::{ + input::{ + keyboard::KeyboardInput, + mouse::{MouseButtonInput, MouseScrollUnit, MouseWheel}, + ButtonState, + }, + prelude::*, +}; + +use crate::{ + context::{Context, CustomEventReader}, + event_dispatcher::EventDispatcher, + input_event::InputEvent, +}; + +pub(crate) fn process_events(world: &mut World) { + let window_size = if let Some(windows) = world.get_resource::<Windows>() { + if let Some(window) = windows.get_primary() { + Vec2::new(window.width(), window.height()) + } else { + // log::warn!("Couldn't find primiary window!"); + return; + } + } else { + // log::warn!("Couldn't find primiary window!"); + return; + }; + + let mut input_events = Vec::new(); + + query_world::< + ( + Res<Events<CursorMoved>>, + Res<Events<MouseButtonInput>>, + Res<Events<MouseWheel>>, + Res<Events<ReceivedCharacter>>, + Res<Events<KeyboardInput>>, + ResMut<CustomEventReader<CursorMoved>>, + ResMut<CustomEventReader<MouseButtonInput>>, + ResMut<CustomEventReader<MouseWheel>>, + ResMut<CustomEventReader<ReceivedCharacter>>, + ResMut<CustomEventReader<KeyboardInput>>, + ), + _, + _, + >( + |( + cursor_moved_events, + mouse_button_input_events, + mouse_wheel_events, + char_input_events, + keyboard_input_events, + mut custom_event_reader_cursor, + mut custom_event_mouse_button, + mut custom_event_mouse_wheel, + mut custom_event_char_input, + mut custom_event_keyboard, + )| { + if let Some(event) = custom_event_reader_cursor + .0 + .iter(&cursor_moved_events) + .last() + { + // Currently, we can only handle a single MouseMoved event at a time so everything but the last needs to be skipped + input_events.push(InputEvent::MouseMoved(( + event.position.x as f32, + window_size.y - event.position.y as f32, + ))); + } + + for event in custom_event_mouse_button.0.iter(&mouse_button_input_events) { + match event.button { + MouseButton::Left => { + if event.state == ButtonState::Pressed { + input_events.push(InputEvent::MouseLeftPress); + } else if event.state == ButtonState::Released { + input_events.push(InputEvent::MouseLeftRelease); + } + } + _ => {} + } + } + + for MouseWheel { x, y, unit } in custom_event_mouse_wheel.0.iter(&mouse_wheel_events) { + input_events.push(InputEvent::Scroll { + dx: *x, + dy: *y, + is_line: matches!(unit, MouseScrollUnit::Line), + }) + } + + for event in custom_event_char_input.0.iter(&char_input_events) { + input_events.push(InputEvent::CharEvent { c: event.char }); + } + + for event in custom_event_keyboard.0.iter(&keyboard_input_events) { + if let Some(key_code) = event.key_code { + input_events.push(InputEvent::Keyboard { + key: key_code, + is_pressed: matches!(event.state, ButtonState::Pressed), + }); + } + } + }, + world, + ); + + world.resource_scope::<EventDispatcher, _>(|world, mut event_dispatcher| { + world.resource_scope::<Context, _>(|world, mut context| { + event_dispatcher.process_events(input_events, &mut context, world); + }); + }); +} + +fn query_world<T: bevy::ecs::system::SystemParam + 'static, F, R>(mut f: F, world: &mut World) -> R +where + F: FnMut(<T::Fetch as bevy::ecs::system::SystemParamFetch<'_, '_>>::Item) -> R, +{ + let mut system_state = bevy::ecs::system::SystemState::<T>::new(world); + let r = { + let test = system_state.get_mut(world); + f(test) + }; + system_state.apply(world); + + r +} diff --git a/kayak_core/src/input_event.rs b/src/input_event.rs similarity index 95% rename from kayak_core/src/input_event.rs rename to src/input_event.rs index 52ad667..6bb7ec7 100644 --- a/kayak_core/src/input_event.rs +++ b/src/input_event.rs @@ -1,43 +1,43 @@ -use crate::KeyCode; - -/// Events sent to [`KayakContext`](crate::KayakContext) containing user input data -#[derive(Debug, PartialEq)] -pub enum InputEvent { - /// An event that occurs when the user moves the mouse - MouseMoved((f32, f32)), - /// An event that occurs when the user presses the left mouse button - MouseLeftPress, - /// An event that occurs when the user releases the left mouse button - MouseLeftRelease, - /// An event that occurs when the user scrolls - Scroll { dx: f32, dy: f32, is_line: bool }, - /// An event that occurs when the user types in a character - CharEvent { c: char }, - /// An event that occurs when the user presses or releases a key - Keyboard { key: KeyCode, is_pressed: bool }, -} - -/// The various categories an input event can belong to -pub enum InputEventCategory { - /// A category for events related to the mouse/cursor - Mouse, - /// A category for events related to the keyboard - Keyboard, - // TODO: Gamepad, etc. -} - -impl InputEvent { - /// Get the category of this input event - pub fn category(&self) -> InputEventCategory { - match self { - // Mouse events - Self::MouseMoved(..) => InputEventCategory::Mouse, - Self::MouseLeftPress => InputEventCategory::Mouse, - Self::MouseLeftRelease => InputEventCategory::Mouse, - Self::Scroll { .. } => InputEventCategory::Mouse, - // Keyboard events - Self::CharEvent { .. } => InputEventCategory::Keyboard, - Self::Keyboard { .. } => InputEventCategory::Keyboard, - } - } -} +use bevy::prelude::KeyCode; + +/// Events sent to [`KayakContext`](crate::KayakContext) containing user input data +#[derive(Debug, PartialEq)] +pub enum InputEvent { + /// An event that occurs when the user moves the mouse + MouseMoved((f32, f32)), + /// An event that occurs when the user presses the left mouse button + MouseLeftPress, + /// An event that occurs when the user releases the left mouse button + MouseLeftRelease, + /// An event that occurs when the user scrolls + Scroll { dx: f32, dy: f32, is_line: bool }, + /// An event that occurs when the user types in a character + CharEvent { c: char }, + /// An event that occurs when the user presses or releases a key + Keyboard { key: KeyCode, is_pressed: bool }, +} + +/// The various categories an input event can belong to +pub enum InputEventCategory { + /// A category for events related to the mouse/cursor + Mouse, + /// A category for events related to the keyboard + Keyboard, + // TODO: Gamepad, etc. +} + +impl InputEvent { + /// Get the category of this input event + pub fn category(&self) -> InputEventCategory { + match self { + // Mouse events + Self::MouseMoved(..) => InputEventCategory::Mouse, + Self::MouseLeftPress => InputEventCategory::Mouse, + Self::MouseLeftRelease => InputEventCategory::Mouse, + Self::Scroll { .. } => InputEventCategory::Mouse, + // Keyboard events + Self::CharEvent { .. } => InputEventCategory::Keyboard, + Self::Keyboard { .. } => InputEventCategory::Keyboard, + } + } +} diff --git a/kayak_core/src/keyboard.rs b/src/keyboard_event.rs similarity index 95% rename from kayak_core/src/keyboard.rs rename to src/keyboard_event.rs index 73e0d1a..204be2f 100644 --- a/kayak_core/src/keyboard.rs +++ b/src/keyboard_event.rs @@ -1,59 +1,59 @@ -use crate::KeyCode; - -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] -pub struct KeyboardModifiers { - /// True if the one of the Control keys is currently pressed - pub is_ctrl_pressed: bool, - /// True if the one of the Shift keys is currently pressed - pub is_shift_pressed: bool, - /// True if the one of the Alt (or "Option") keys is currently pressed - pub is_alt_pressed: bool, - /// True if the one of the Meta keys is currently pressed - /// - /// This is the "Command" ("⌘") key on Mac and "Windows" or "Super" on other systems. - pub is_meta_pressed: bool, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct KeyboardEvent { - key: KeyCode, - modifiers: KeyboardModifiers, -} - -impl KeyboardEvent { - pub fn new(key: KeyCode, modifiers: KeyboardModifiers) -> Self { - Self { key, modifiers } - } - - /// Returns this event's affected key - pub fn key(&self) -> KeyCode { - self.key - } - - /// Returns all modifiers for this event's key - pub fn modifiers(&self) -> KeyboardModifiers { - self.modifiers - } - - /// Returns true if the one of the Control keys is currently pressed - pub fn is_ctrl_pressed(&self) -> bool { - self.modifiers.is_ctrl_pressed - } - - /// Returns true if the one of the Shift keys is currently pressed - pub fn is_shift_pressed(&self) -> bool { - self.modifiers.is_shift_pressed - } - - /// Returns true if the one of the Alt (or "Option") keys is currently pressed - pub fn is_alt_pressed(&self) -> bool { - self.modifiers.is_alt_pressed - } - - /// Returns true if the one of the Meta keys is currently pressed - /// - /// This is the "Command" ("⌘") key on Mac and "Windows" or "Super" on other systems. - pub fn is_meta_pressed(&self) -> bool { - self.modifiers.is_meta_pressed - } -} +use bevy::prelude::KeyCode; + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub struct KeyboardModifiers { + /// True if the one of the Control keys is currently pressed + pub is_ctrl_pressed: bool, + /// True if the one of the Shift keys is currently pressed + pub is_shift_pressed: bool, + /// True if the one of the Alt (or "Option") keys is currently pressed + pub is_alt_pressed: bool, + /// True if the one of the Meta keys is currently pressed + /// + /// This is the "Command" ("⌘") key on Mac and "Windows" or "Super" on other systems. + pub is_meta_pressed: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct KeyboardEvent { + key: KeyCode, + modifiers: KeyboardModifiers, +} + +impl KeyboardEvent { + pub fn new(key: KeyCode, modifiers: KeyboardModifiers) -> Self { + Self { key, modifiers } + } + + /// Returns this event's affected key + pub fn key(&self) -> KeyCode { + self.key + } + + /// Returns all modifiers for this event's key + pub fn modifiers(&self) -> KeyboardModifiers { + self.modifiers + } + + /// Returns true if the one of the Control keys is currently pressed + pub fn is_ctrl_pressed(&self) -> bool { + self.modifiers.is_ctrl_pressed + } + + /// Returns true if the one of the Shift keys is currently pressed + pub fn is_shift_pressed(&self) -> bool { + self.modifiers.is_shift_pressed + } + + /// Returns true if the one of the Alt (or "Option") keys is currently pressed + pub fn is_alt_pressed(&self) -> bool { + self.modifiers.is_alt_pressed + } + + /// Returns true if the one of the Meta keys is currently pressed + /// + /// This is the "Command" ("⌘") key on Mac and "Windows" or "Super" on other systems. + pub fn is_meta_pressed(&self) -> bool { + self.modifiers.is_meta_pressed + } +} diff --git a/kayak_core/src/layout_cache.rs b/src/layout.rs similarity index 52% rename from kayak_core/src/layout_cache.rs rename to src/layout.rs index 5f59f19..c09176e 100644 --- a/kayak_core/src/layout_cache.rs +++ b/src/layout.rs @@ -1,9 +1,11 @@ use std::collections::hash_map::Iter; use std::collections::HashMap; -use morphorm::{Cache, GeometryChanged}; +use bevy::prelude::{Entity, Query}; +use morphorm::Cache; +pub use morphorm::GeometryChanged; -use crate::Index; +use crate::node::WrappedIndex; #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Rect { @@ -36,42 +38,42 @@ pub struct Size { } #[derive(Default, Debug)] -pub struct LayoutCache { +pub(crate) struct LayoutCache { // Computed Outputs - pub rect: HashMap<Index, Rect>, + pub rect: HashMap<WrappedIndex, Rect>, // Intermediate Values - space: HashMap<Index, Space>, - size: HashMap<Index, Size>, + space: HashMap<WrappedIndex, Space>, + size: HashMap<WrappedIndex, Size>, - child_width_max: HashMap<Index, f32>, - child_height_max: HashMap<Index, f32>, - child_width_sum: HashMap<Index, f32>, - child_height_sum: HashMap<Index, f32>, + child_width_max: HashMap<WrappedIndex, f32>, + child_height_max: HashMap<WrappedIndex, f32>, + child_width_sum: HashMap<WrappedIndex, f32>, + child_height_sum: HashMap<WrappedIndex, f32>, - grid_row_max: HashMap<Index, f32>, - grid_col_max: HashMap<Index, f32>, + grid_row_max: HashMap<WrappedIndex, f32>, + grid_col_max: HashMap<WrappedIndex, f32>, - horizontal_free_space: HashMap<Index, f32>, - horizontal_stretch_sum: HashMap<Index, f32>, + horizontal_free_space: HashMap<WrappedIndex, f32>, + horizontal_stretch_sum: HashMap<WrappedIndex, f32>, - vertical_free_space: HashMap<Index, f32>, - vertical_stretch_sum: HashMap<Index, f32>, + vertical_free_space: HashMap<WrappedIndex, f32>, + vertical_stretch_sum: HashMap<WrappedIndex, f32>, - stack_first_child: HashMap<Index, bool>, - stack_last_child: HashMap<Index, bool>, + stack_first_child: HashMap<WrappedIndex, bool>, + stack_last_child: HashMap<WrappedIndex, bool>, /// A map of node IDs to their `GeometryChanged` flags /// /// This should only contain entries for nodes that have _at least one_ flag set. /// If a node does not have any flags set, then they should be removed from the map. - geometry_changed: HashMap<Index, GeometryChanged>, + geometry_changed: HashMap<WrappedIndex, GeometryChanged>, - visible: HashMap<Index, bool>, + visible: HashMap<WrappedIndex, bool>, } impl LayoutCache { - pub fn add(&mut self, node_index: Index) { + pub fn add(&mut self, node_index: WrappedIndex) { self.space.insert(node_index, Default::default()); self.child_width_max.insert(node_index, Default::default()); @@ -102,21 +104,26 @@ impl LayoutCache { } /// Attempts to initialize the node if it hasn't already been initialized. - fn try_init(&mut self, node: Index) { + fn try_init(&mut self, node: WrappedIndex) { self.rect.entry(node).or_default(); } /// Returns an iterator over nodes whose layout have been changed since the last update - pub fn iter_changed(&self) -> Iter<'_, Index, GeometryChanged> { + pub fn iter_changed(&self) -> Iter<'_, WrappedIndex, GeometryChanged> { self.geometry_changed.iter() } } -impl Cache for LayoutCache { - type Item = Index; +pub(crate) struct DataCache<'borrow, 'world, 'state> { + pub query: &'borrow Query<'world, 'state, &'static crate::node::Node>, + pub cache: &'borrow mut LayoutCache, +} + +impl<'b, 'w, 's> Cache for DataCache<'b, 'w, 's> { + type Item = WrappedIndex; fn visible(&self, node: Self::Item) -> bool { - if let Some(value) = self.visible.get(&node) { + if let Some(value) = self.cache.visible.get(&node) { return *value; } @@ -124,7 +131,7 @@ impl Cache for LayoutCache { } fn geometry_changed(&self, node: Self::Item) -> GeometryChanged { - if let Some(geometry_changed) = self.geometry_changed.get(&node) { + if let Some(geometry_changed) = self.cache.geometry_changed.get(&node) { return *geometry_changed; } @@ -133,26 +140,25 @@ impl Cache for LayoutCache { fn set_geo_changed(&mut self, node: Self::Item, flag: GeometryChanged, value: bool) { // This method is guaranteed to be called by morphorm every layout so we'll attempt to initialize here - self.try_init(node); - + self.cache.try_init(node); if value { // Setting a flag -> Add entry if it does not already exist - let geometry_changed = self.geometry_changed.entry(node).or_default(); + let geometry_changed = self.cache.geometry_changed.entry(node).or_default(); geometry_changed.set(flag, value); } else { // Unsetting a flag -> Don't add entry if it does not exist - if let Some(geometry_changed) = self.geometry_changed.get_mut(&node) { + if let Some(geometry_changed) = self.cache.geometry_changed.get_mut(&node) { geometry_changed.set(flag, value); if geometry_changed.is_empty() { - self.geometry_changed.remove(&node); + self.cache.geometry_changed.remove(&node); } } } } fn width(&self, node: Self::Item) -> f32 { - if let Some(rect) = self.rect.get(&node) { + if let Some(rect) = self.cache.rect.get(&node) { return rect.width; } @@ -160,7 +166,7 @@ impl Cache for LayoutCache { } fn height(&self, node: Self::Item) -> f32 { - if let Some(rect) = self.rect.get(&node) { + if let Some(rect) = self.cache.rect.get(&node) { return rect.height; } @@ -168,7 +174,7 @@ impl Cache for LayoutCache { } fn posx(&self, node: Self::Item) -> f32 { - if let Some(rect) = self.rect.get(&node) { + if let Some(rect) = self.cache.rect.get(&node) { return rect.posx; } @@ -176,7 +182,7 @@ impl Cache for LayoutCache { } fn posy(&self, node: Self::Item) -> f32 { - if let Some(rect) = self.rect.get(&node) { + if let Some(rect) = self.cache.rect.get(&node) { return rect.posy; } @@ -184,7 +190,7 @@ impl Cache for LayoutCache { } fn left(&self, node: Self::Item) -> f32 { - if let Some(space) = self.space.get(&node) { + if let Some(space) = self.cache.space.get(&node) { return space.left; } @@ -192,7 +198,7 @@ impl Cache for LayoutCache { } fn right(&self, node: Self::Item) -> f32 { - if let Some(space) = self.space.get(&node) { + if let Some(space) = self.cache.space.get(&node) { return space.right; } @@ -200,7 +206,7 @@ impl Cache for LayoutCache { } fn top(&self, node: Self::Item) -> f32 { - if let Some(space) = self.space.get(&node) { + if let Some(space) = self.cache.space.get(&node) { return space.top; } @@ -208,7 +214,7 @@ impl Cache for LayoutCache { } fn bottom(&self, node: Self::Item) -> f32 { - if let Some(space) = self.space.get(&node) { + if let Some(space) = self.cache.space.get(&node) { return space.bottom; } @@ -216,7 +222,7 @@ impl Cache for LayoutCache { } fn new_width(&self, node: Self::Item) -> f32 { - if let Some(size) = self.size.get(&node) { + if let Some(size) = self.cache.size.get(&node) { return size.width; } @@ -224,7 +230,7 @@ impl Cache for LayoutCache { } fn new_height(&self, node: Self::Item) -> f32 { - if let Some(size) = self.size.get(&node) { + if let Some(size) = self.cache.size.get(&node) { return size.height; } @@ -232,155 +238,238 @@ impl Cache for LayoutCache { } fn child_width_max(&self, node: Self::Item) -> f32 { - *self.child_width_max.get(&node).unwrap() + *self.cache.child_width_max.get(&node).unwrap() } /// Get the computed sum of the widths of the child nodes fn child_width_sum(&self, node: Self::Item) -> f32 { - *self.child_width_sum.get(&node).unwrap() + *self.cache.child_width_sum.get(&node).unwrap() } /// Get the computed maximum width of the child nodes fn child_height_max(&self, node: Self::Item) -> f32 { - *self.child_height_max.get(&node).unwrap() + *self.cache.child_height_max.get(&node).unwrap() } /// Get the computed sum of the widths of the child nodes fn child_height_sum(&self, node: Self::Item) -> f32 { - *self.child_height_sum.get(&node).unwrap() + *self.cache.child_height_sum.get(&node).unwrap() } /// Get the computed maximum grid row fn grid_row_max(&self, node: Self::Item) -> f32 { - *self.grid_row_max.get(&node).unwrap() + *self.cache.grid_row_max.get(&node).unwrap() } /// Get the computed maximum grid column fn grid_col_max(&self, node: Self::Item) -> f32 { - *self.grid_col_max.get(&node).unwrap() + *self.cache.grid_col_max.get(&node).unwrap() } // Setters fn set_visible(&mut self, node: Self::Item, value: bool) { - *self.visible.get_mut(&node).unwrap() = value; + *self.cache.visible.get_mut(&node).unwrap() = value; } fn set_child_width_sum(&mut self, node: Self::Item, value: f32) { - *self.child_width_sum.get_mut(&node).unwrap() = value; + *self.cache.child_width_sum.get_mut(&node).unwrap() = value; } fn set_child_height_sum(&mut self, node: Self::Item, value: f32) { - *self.child_height_sum.get_mut(&node).unwrap() = value; + *self.cache.child_height_sum.get_mut(&node).unwrap() = value; } fn set_child_width_max(&mut self, node: Self::Item, value: f32) { - *self.child_width_max.get_mut(&node).unwrap() = value; + *self.cache.child_width_max.get_mut(&node).unwrap() = value; } fn set_child_height_max(&mut self, node: Self::Item, value: f32) { - *self.child_height_max.get_mut(&node).unwrap() = value; + *self.cache.child_height_max.get_mut(&node).unwrap() = value; } fn horizontal_free_space(&self, node: Self::Item) -> f32 { - *self.horizontal_free_space.get(&node).unwrap() + *self.cache.horizontal_free_space.get(&node).unwrap() } fn set_horizontal_free_space(&mut self, node: Self::Item, value: f32) { - *self.horizontal_free_space.get_mut(&node).unwrap() = value; + *self.cache.horizontal_free_space.get_mut(&node).unwrap() = value; } fn vertical_free_space(&self, node: Self::Item) -> f32 { - *self.vertical_free_space.get(&node).unwrap() + *self.cache.vertical_free_space.get(&node).unwrap() } fn set_vertical_free_space(&mut self, node: Self::Item, value: f32) { - *self.vertical_free_space.get_mut(&node).unwrap() = value; + *self.cache.vertical_free_space.get_mut(&node).unwrap() = value; } fn horizontal_stretch_sum(&self, node: Self::Item) -> f32 { - *self.horizontal_stretch_sum.get(&node).unwrap() + *self.cache.horizontal_stretch_sum.get(&node).unwrap() } fn set_horizontal_stretch_sum(&mut self, node: Self::Item, value: f32) { - *self.horizontal_stretch_sum.get_mut(&node).unwrap() = value; + *self.cache.horizontal_stretch_sum.get_mut(&node).unwrap() = value; } fn vertical_stretch_sum(&self, node: Self::Item) -> f32 { - *self.vertical_stretch_sum.get(&node).unwrap() + *self.cache.vertical_stretch_sum.get(&node).unwrap() } fn set_vertical_stretch_sum(&mut self, node: Self::Item, value: f32) { - *self.vertical_stretch_sum.get_mut(&node).unwrap() = value; + *self.cache.vertical_stretch_sum.get_mut(&node).unwrap() = value; } fn set_grid_row_max(&mut self, node: Self::Item, value: f32) { - *self.grid_row_max.get_mut(&node).unwrap() = value; + *self.cache.grid_row_max.get_mut(&node).unwrap() = value; } fn set_grid_col_max(&mut self, node: Self::Item, value: f32) { - *self.grid_row_max.get_mut(&node).unwrap() = value; + *self.cache.grid_row_max.get_mut(&node).unwrap() = value; } fn set_width(&mut self, node: Self::Item, value: f32) { - let rect = self.rect.entry(node).or_default(); + let rect = self.cache.rect.entry(node).or_default(); rect.width = value; } fn set_height(&mut self, node: Self::Item, value: f32) { - let rect = self.rect.entry(node).or_default(); + let rect = self.cache.rect.entry(node).or_default(); rect.height = value; } fn set_posx(&mut self, node: Self::Item, value: f32) { - let rect = self.rect.entry(node).or_default(); + let rect = self.cache.rect.entry(node).or_default(); rect.posx = value; } fn set_posy(&mut self, node: Self::Item, value: f32) { - let rect = self.rect.entry(node).or_default(); + let rect = self.cache.rect.entry(node).or_default(); rect.posy = value; } fn set_left(&mut self, node: Self::Item, value: f32) { - if let Some(space) = self.space.get_mut(&node) { + if let Some(space) = self.cache.space.get_mut(&node) { space.left = value; } } fn set_right(&mut self, node: Self::Item, value: f32) { - if let Some(space) = self.space.get_mut(&node) { + if let Some(space) = self.cache.space.get_mut(&node) { space.right = value; } } fn set_top(&mut self, node: Self::Item, value: f32) { - if let Some(space) = self.space.get_mut(&node) { + if let Some(space) = self.cache.space.get_mut(&node) { space.top = value; } } fn set_bottom(&mut self, node: Self::Item, value: f32) { - if let Some(space) = self.space.get_mut(&node) { + if let Some(space) = self.cache.space.get_mut(&node) { space.bottom = value; } } fn set_new_width(&mut self, node: Self::Item, value: f32) { - if let Some(size) = self.size.get_mut(&node) { + if let Some(size) = self.cache.size.get_mut(&node) { size.width = value; } } fn set_new_height(&mut self, node: Self::Item, value: f32) { - if let Some(size) = self.size.get_mut(&node) { + if let Some(size) = self.cache.size.get_mut(&node) { size.height = value; } } fn stack_first_child(&self, node: Self::Item) -> bool { - *self.stack_first_child.get(&node).unwrap() + *self.cache.stack_first_child.get(&node).unwrap() } fn set_stack_first_child(&mut self, node: Self::Item, value: bool) { - *self.stack_first_child.get_mut(&node).unwrap() = value; + *self.cache.stack_first_child.get_mut(&node).unwrap() = value; } fn stack_last_child(&self, node: Self::Item) -> bool { - *self.stack_last_child.get(&node).unwrap() + *self.cache.stack_last_child.get(&node).unwrap() } fn set_stack_last_child(&mut self, node: Self::Item, value: bool) { - *self.stack_last_child.get_mut(&node).unwrap() = value; + *self.cache.stack_last_child.get_mut(&node).unwrap() = value; + } +} + +/// A layout data sent to widgets on layout. +/// +/// Similar and interchangeable with [Rect] +/// ``` +/// use kayak_core::layout_cache::Rect; +/// use kayak_core::Layout; +/// +/// let layout = Layout::default(); +/// let rect : Rect = layout.into(); +/// let layout : Layout = rect.into(); +/// ``` +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Layout { + /// width of the component + pub width: f32, + /// height of the component + pub height: f32, + /// x-coordinates of the component + pub x: f32, + /// y-coordinates of the component + pub y: f32, + /// z-coordinates of the component + pub z: f32, +} + +impl Layout { + /// Returns the position as a Kayak position type + pub fn pos(&self) -> (f32, f32) { + (self.x, self.y) + } +} + +impl From<Layout> for Rect { + fn from(layout: Layout) -> Self { + Rect { + posx: layout.x, + posy: layout.y, + width: layout.width, + height: layout.height, + z_index: layout.z, + } + } +} + +impl From<Rect> for Layout { + fn from(rect: Rect) -> Self { + Layout { + width: rect.width, + height: rect.height, + x: rect.posx, + y: rect.posy, + z: rect.z_index, + } + } +} + +/// +/// Struct used for [crate::OnLayout] as layout event data. +/// +pub struct LayoutEvent { + /// Layout of target component + pub layout: Layout, + /// Flags denoting the layout change. + /// + /// Note: The flags can potentially all be unset in cases where the [target's] layout did + /// not change, but one of its immediate children did. + /// + /// [target's]: LayoutEvent::target + pub flags: GeometryChanged, + /// The node ID of the element receiving the layout event. + pub target: Entity, +} + +impl LayoutEvent { + pub(crate) fn new(rect: Rect, geometry_change: GeometryChanged, index: Entity) -> LayoutEvent { + LayoutEvent { + layout: rect.into(), + flags: geometry_change, + target: index, + } } } diff --git a/src/layout_dispatcher.rs b/src/layout_dispatcher.rs new file mode 100644 index 0000000..69e8c81 --- /dev/null +++ b/src/layout_dispatcher.rs @@ -0,0 +1,83 @@ +use bevy::{ + prelude::{Entity, With, World}, + utils::HashSet, +}; +use indexmap::IndexMap; +use morphorm::GeometryChanged; + +use crate::{ + layout::{LayoutCache, LayoutEvent}, + node::WrappedIndex, + on_layout::OnLayout, + prelude::Context, +}; + +pub(crate) struct LayoutEventDispatcher; + +impl LayoutEventDispatcher { + pub fn dispatch(context: &mut Context, world: &mut World) { + let on_event_entities = { + let mut query = world.query_filtered::<Entity, With<OnLayout>>(); + query + .iter(world) + .map(|entity| entity) + .collect::<HashSet<_>>() + }; + + if let Ok(layout_cache) = context.layout_cache.try_read() { + let changed = layout_cache.iter_changed(); + let changed = changed + .filter_map(|(index, flags)| { + if on_event_entities.contains(&index.0) { + Some((*index, *flags)) + } else { + None + } + }) + .collect::<IndexMap<WrappedIndex, GeometryChanged>>(); + + // Use IndexSet to prevent duplicates and maintain speed + let mut parents: IndexMap<WrappedIndex, GeometryChanged> = IndexMap::default(); + + if let Ok(tree) = context.tree.try_read() { + for (node_index, flags) in &changed { + // Add parent to set + if let Some(parent_index) = tree.get_parent(*node_index) { + if !changed.contains_key(&parent_index) { + parents.insert(parent_index, GeometryChanged::default()); + } + } + + // Process and dispatch + Self::process(world, &layout_cache, *node_index, *flags); + } + } + + // Finally, process all parents + for (parent_index, flags) in parents { + // Process and dispatch + Self::process(world, &layout_cache, parent_index, flags); + } + } + } + + fn process( + world: &mut World, + layout_cache: &LayoutCache, + index: WrappedIndex, + flags: GeometryChanged, + ) { + // We should be able to just get layout from WidgetManager here + // since the layouts will be calculated by this point + if let Some(mut entity) = world.get_entity_mut(index.0) { + if let Some(mut on_layout) = entity.remove::<OnLayout>() { + if let Some(rect) = layout_cache.rect.get(&index) { + // dbg!(format!("Processing event for: {:?}", entity.id())); + let layout_event = LayoutEvent::new(*rect, flags, index.0); + on_layout.try_call(index.0, layout_event, world); + world.entity_mut(index.0).insert(on_layout); + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1b70e67..9c33444 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,63 @@ -/// The core of Kayak UI, containing all required code -pub mod core { - pub use kayak_core::*; - pub use kayak_render_macros::{ - constructor, render, rsx, use_effect, use_state, widget, WidgetProps, - }; -} +#![allow(dead_code)] -/// Contains code related to loading and reading fonts in Kayak UI -#[cfg(feature = "bevy_renderer")] -pub mod font { - pub use kayak_font::*; -} +mod calculate_nodes; +mod camera; +mod children; +mod context; +mod context_entities; +mod cursor; +mod event; +mod event_dispatcher; +mod focus_tree; +mod input; +mod input_event; +mod keyboard_event; +mod layout; +mod layout_dispatcher; +mod node; +mod on_change; +mod on_event; +mod on_layout; +pub(crate) mod render; +mod render_primitive; +mod styles; +mod tree; +mod widget; +mod widget_context; +mod widgets; +mod window_size; + +pub use window_size::WindowSize; + +pub use camera::*; + +/// The default font name used by Kayak +pub const DEFAULT_FONT: &str = "Kayak-Default"; -/// Bevy-specific code for Bevy integration -#[cfg(feature = "bevy_renderer")] -pub mod bevy { - pub use bevy_kayak_ui::*; +pub mod prelude { + pub use crate::camera::UICameraBundle; + pub use crate::children::KChildren; + pub use crate::context::*; + pub use crate::render::font::FontMapping; + pub use crate::tree::*; + pub mod widgets { + pub use crate::widgets::*; + } + pub use crate::event::*; + pub use crate::event_dispatcher::EventDispatcherContext; + pub use crate::focus_tree::Focusable; + pub use crate::input_event::*; + pub use crate::keyboard_event::*; + pub use crate::layout::*; + pub use crate::node::DirtyNode; + pub use crate::on_change::OnChange; + pub use crate::on_event::OnEvent; + pub use crate::on_layout::OnLayout; + pub use crate::styles::*; + pub use crate::widget::*; + pub use crate::widget_context::*; + pub use kayak_font::Alignment; + pub use kayak_ui_macros::{constructor, rsx}; } -/// A convenient collection of built-in widgets -pub mod widgets; +pub use focus_tree::Focusable; diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 0000000..0fb1acc --- /dev/null +++ b/src/node.rs @@ -0,0 +1,441 @@ +use bevy::prelude::{Component, Entity, Query}; + +use crate::{ + render_primitive::RenderPrimitive, + styles::{KStyle, StyleProp}, +}; + +#[derive(Component, Debug, Clone, Copy)] +pub struct DirtyNode; + +/// A widget node used for building the layout tree +#[derive(Debug, Clone, PartialEq, Component)] +pub struct Node { + /// The list of children directly under this node + pub children: Vec<WrappedIndex>, + /// The ID of this node's widget + pub id: WrappedIndex, + /// The fully resolved styles for this node + pub resolved_styles: KStyle, + /// The raw styles for this node, before style resolution + pub raw_styles: Option<KStyle>, + /// The generated [`RenderPrimitive`] of this node + pub primitive: RenderPrimitive, + /// The z-index of this node, used for controlling layering + pub z: f32, +} + +impl Default for Node { + fn default() -> Self { + Self { + children: Default::default(), + id: WrappedIndex(Entity::from_raw(0)), + resolved_styles: Default::default(), + raw_styles: Default::default(), + primitive: RenderPrimitive::Empty, + z: Default::default(), + } + } +} + +/// A struct used for building a [`Node`] +pub struct NodeBuilder { + node: Node, +} + +impl NodeBuilder { + /// Defines a basic node without children, styles, etc. + pub fn empty() -> Self { + Self { + node: Node { + children: Vec::new(), + id: WrappedIndex(Entity::from_raw(0)), + resolved_styles: KStyle::default(), + raw_styles: None, + primitive: RenderPrimitive::Empty, + z: 0.0, + }, + } + } + + /// Defines a node with the given id and styles + pub fn new(id: WrappedIndex, styles: KStyle) -> Self { + Self { + node: Node { + children: Vec::new(), + id, + resolved_styles: styles, + raw_styles: None, + primitive: RenderPrimitive::Empty, + z: 0.0, + }, + } + } + + /// Sets the ID of the node being built + pub fn with_id(mut self, id: WrappedIndex) -> Self { + self.node.id = id; + self + } + + /// Sets the children of the node being built + pub fn with_children(mut self, children: Vec<WrappedIndex>) -> Self { + self.node.children.extend(children); + self + } + + /// Sets the resolved and raw styles, respectively, of the node being built + pub fn with_styles(mut self, resolved_styles: KStyle, raw_styles: Option<KStyle>) -> Self { + self.node.resolved_styles = resolved_styles; + self.node.raw_styles = raw_styles; + self + } + + /// Sets the [`RenderPrimitive`] of the node being built + pub fn with_primitive(mut self, primitive: RenderPrimitive) -> Self { + self.node.primitive = primitive; + self + } + + /// Completes and builds the actual [`Node`] + pub fn build(self) -> Node { + self.node + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct WrappedIndex(pub Entity); + +impl<'a> morphorm::Node<'a> for WrappedIndex { + type Data = Query<'a, 'a, &'static Node>; + + fn layout_type(&self, store: &'_ Self::Data) -> Option<morphorm::LayoutType> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.layout_type { + StyleProp::Default => Some(morphorm::LayoutType::default()), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::LayoutType::default()), + }; + } + return Some(morphorm::LayoutType::default()); + } + + fn position_type(&self, store: &'_ Self::Data) -> Option<morphorm::PositionType> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.position_type { + StyleProp::Default => Some(morphorm::PositionType::default()), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::PositionType::default()), + }; + } + return Some(morphorm::PositionType::default()); + } + + fn width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.width { + StyleProp::Default => Some(morphorm::Units::Stretch(1.0)), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Stretch(1.0)), + }; + } + return Some(morphorm::Units::Stretch(1.0)); + } + + fn height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.height { + StyleProp::Default => Some(morphorm::Units::Stretch(1.0)), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Stretch(1.0)), + }; + } + return Some(morphorm::Units::Stretch(1.0)); + } + + fn min_width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.min_width { + StyleProp::Default => Some(morphorm::Units::Pixels(0.0)), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn min_height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.min_height { + StyleProp::Default => Some(morphorm::Units::Pixels(0.0)), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn max_width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.max_width { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn max_height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.max_height { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.left { + StyleProp::Default => match node.resolved_styles.offset { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.left), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.right { + StyleProp::Default => match node.resolved_styles.offset { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.right), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.top { + StyleProp::Default => match node.resolved_styles.offset { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.top), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.bottom { + StyleProp::Default => match node.resolved_styles.offset { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.bottom), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn min_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn max_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn min_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn max_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn min_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn max_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn min_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn max_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> { + Some(morphorm::Units::Auto) + } + + fn child_left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.padding_left { + StyleProp::Default => match node.resolved_styles.padding { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.left), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn child_right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.padding_right { + StyleProp::Default => match node.resolved_styles.padding { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.right), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn child_top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.padding_top { + StyleProp::Default => match node.resolved_styles.padding { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.top), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn child_bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.padding_bottom { + StyleProp::Default => match node.resolved_styles.padding { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop.bottom), + _ => Some(morphorm::Units::Auto), + }, + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + return Some(morphorm::Units::Auto); + } + + fn row_between(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.row_between { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn col_between(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.col_between { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(prop), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn grid_rows(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> { + Some(vec![]) + } + + fn grid_cols(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> { + Some(vec![]) + } + + fn row_index(&self, _store: &'_ Self::Data) -> Option<usize> { + Some(0) + } + + fn col_index(&self, _store: &'_ Self::Data) -> Option<usize> { + Some(0) + } + + fn row_span(&self, _store: &'_ Self::Data) -> Option<usize> { + Some(1) + } + + fn col_span(&self, _store: &'_ Self::Data) -> Option<usize> { + Some(1) + } + + fn border_left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.border { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.left)), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn border_right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.border { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.right)), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn border_top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.border { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.top)), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } + + fn border_bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> { + if let Ok(node) = store.get(self.0) { + return match node.resolved_styles.border { + StyleProp::Default => Some(morphorm::Units::Auto), + StyleProp::Value(prop) => Some(morphorm::Units::Pixels(prop.bottom)), + _ => Some(morphorm::Units::Auto), + }; + } + Some(morphorm::Units::Auto) + } +} diff --git a/src/on_change.rs b/src/on_change.rs new file mode 100644 index 0000000..1bdadbf --- /dev/null +++ b/src/on_change.rs @@ -0,0 +1,79 @@ +use bevy::ecs::component::TableStorage; +use bevy::prelude::{Component, Entity, In, IntoSystem, System, World}; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, RwLock}; + +use crate::prelude::WidgetContext; + +pub trait ChangeValue: Component<Storage = TableStorage> + Default {} + +/// A container for a function that handles layout +/// +/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly +/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`] +/// as a parameter. +#[derive(Component, Clone)] +pub struct OnChange { + value: Arc<RwLock<String>>, + has_initialized: Arc<RwLock<bool>>, + system: Arc<RwLock<dyn System<In = (WidgetContext, Entity, String), Out = ()>>>, +} + +impl Default for OnChange { + fn default() -> Self { + Self::new(|In(_)| {}) + } +} + +impl OnChange { + /// Create a new layout handler + /// + /// The handler should be a closure that takes the following arguments: + /// 1. The LayoutEvent + pub fn new<Params>( + system: impl IntoSystem<(WidgetContext, Entity, String), (), Params>, + ) -> Self { + Self { + value: Default::default(), + has_initialized: Arc::new(RwLock::new(false)), + system: Arc::new(RwLock::new(IntoSystem::into_system(system))), + } + } + + pub fn set_value(&self, value: String) { + if let Ok(mut value_mut) = self.value.try_write() { + *value_mut = value; + }; + } + + /// Call the layout event handler + /// + /// Returns true if the handler was successfully invoked. + pub fn try_call(&self, entity: Entity, world: &mut World, widget_context: WidgetContext) { + if let Ok(value) = self.value.try_read() { + if let Ok(mut init) = self.has_initialized.try_write() { + if let Ok(mut system) = self.system.try_write() { + if !*init { + system.initialize(world); + *init = true; + } + system.run((widget_context, entity, value.clone()), world); + system.apply_buffers(world); + } + } + } + } +} + +impl Debug for OnChange { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OnLayout").finish() + } +} + +impl PartialEq for OnChange { + fn eq(&self, _: &Self) -> bool { + // Never prevent "==" for being true because of this struct + true + } +} diff --git a/src/on_event.rs b/src/on_event.rs new file mode 100644 index 0000000..2561372 --- /dev/null +++ b/src/on_event.rs @@ -0,0 +1,87 @@ +use bevy::prelude::{Component, Entity, In, IntoSystem, System, World}; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, RwLock}; + +use crate::event::Event; +use crate::event_dispatcher::EventDispatcherContext; + +/// A container for a function that handles events +/// +/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly +/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`] +/// as a parameter. +#[derive(Component, Clone)] +pub struct OnEvent { + has_initialized: bool, + system: Arc< + RwLock< + dyn System< + In = (EventDispatcherContext, Event, Entity), + Out = (EventDispatcherContext, Event), + >, + >, + >, +} + +impl Default for OnEvent { + fn default() -> Self { + Self::new(|In((event_dispatcher_context, event, _entity))| { + (event_dispatcher_context, event) + }) + } +} + +impl OnEvent { + /// Create a new event handler + /// + /// The handler should be a closure that takes the following arguments: + /// 1. The current context + /// 2. The event + pub fn new<Params>( + system: impl IntoSystem< + (EventDispatcherContext, Event, Entity), + (EventDispatcherContext, Event), + Params, + >, + ) -> OnEvent { + Self { + has_initialized: false, + system: Arc::new(RwLock::new(IntoSystem::into_system(system))), + } + } + + /// Call the event handler + /// + /// Returns true if the handler was successfully invoked. + pub fn try_call( + &mut self, + mut event_dispatcher_context: EventDispatcherContext, + entity: Entity, + mut event: Event, + world: &mut World, + ) -> (EventDispatcherContext, Event) { + if let Ok(mut system) = self.system.try_write() { + if !self.has_initialized { + system.initialize(world); + self.has_initialized = true; + } + (event_dispatcher_context, event) = + system.run((event_dispatcher_context, event, entity), world); + system.apply_buffers(world); + } + (event_dispatcher_context, event) + } +} + +impl Debug for OnEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OnEvent").finish() + } +} + +impl PartialEq for OnEvent { + fn eq(&self, _: &Self) -> bool { + // Never prevent "==" for being true because of this struct + true + } +} diff --git a/src/on_layout.rs b/src/on_layout.rs new file mode 100644 index 0000000..43b483a --- /dev/null +++ b/src/on_layout.rs @@ -0,0 +1,70 @@ +use bevy::prelude::{Component, Entity, In, IntoSystem, System, World}; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, RwLock}; + +use crate::layout::LayoutEvent; + +/// A container for a function that handles layout +/// +/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly +/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`] +/// as a parameter. +#[derive(Component, Clone)] +pub struct OnLayout { + has_initialized: bool, + system: Arc<RwLock<dyn System<In = (LayoutEvent, Entity), Out = LayoutEvent>>>, +} + +impl Default for OnLayout { + fn default() -> Self { + Self::new(|In((event, _entity))| event) + } +} + +impl OnLayout { + /// Create a new layout handler + /// + /// The handler should be a closure that takes the following arguments: + /// 1. The LayoutEvent + pub fn new<Params>( + system: impl IntoSystem<(LayoutEvent, Entity), LayoutEvent, Params>, + ) -> Self { + Self { + has_initialized: false, + system: Arc::new(RwLock::new(IntoSystem::into_system(system))), + } + } + + /// Call the layout event handler + /// + /// Returns true if the handler was successfully invoked. + pub fn try_call( + &mut self, + entity: Entity, + mut event: LayoutEvent, + world: &mut World, + ) -> LayoutEvent { + if let Ok(mut system) = self.system.try_write() { + if !self.has_initialized { + system.initialize(world); + self.has_initialized = true; + } + event = system.run((event, entity), world); + system.apply_buffers(world); + } + event + } +} + +impl Debug for OnLayout { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OnLayout").finish() + } +} + +impl PartialEq for OnLayout { + fn eq(&self, _: &Self) -> bool { + // Never prevent "==" for being true because of this struct + true + } +} diff --git a/bevy_kayak_ui/src/render/mod.rs b/src/render/extract.rs similarity index 68% rename from bevy_kayak_ui/src/render/mod.rs rename to src/render/extract.rs index f25188a..fd055e6 100644 --- a/bevy_kayak_ui/src/render/mod.rs +++ b/src/render/extract.rs @@ -1,31 +1,25 @@ -use crate::{BevyContext, FontMapping, ImageManager}; +use crate::{context::Context, node::Node, render_primitive::RenderPrimitive, styles::Corner}; use bevy::{ - math::Vec2, - prelude::{Assets, Commands, Plugin, Res}, - render::{color::Color, texture::Image, Extract, RenderApp, RenderStage}, - sprite::Rect, + // math::Vec2, + prelude::{Assets, Color, Commands, Image, Plugin, Query, Rect, Res, Vec2}, + render::{Extract, RenderApp, RenderStage}, window::Windows, }; -use bevy_kayak_renderer::{ - render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, -}; -use kayak_core::render_primitive::RenderPrimitive; use kayak_font::KayakFont; -pub mod font; -pub mod image; -mod nine_patch; -mod quad; -mod texture_atlas; +use super::{ + font::{self, FontMapping}, + image, nine_patch, texture_atlas, + unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, +}; + +// mod nine_patch; +// mod texture_atlas; pub struct BevyKayakUIExtractPlugin; impl Plugin for BevyKayakUIExtractPlugin { fn build(&self, app: &mut bevy::prelude::App) { - app.add_plugin(font::TextRendererPlugin) - .add_plugin(image::ImageRendererPlugin); - let render_app = app.sub_app_mut(RenderApp); render_app.add_system_to_stage(RenderStage::Extract, extract); } @@ -33,26 +27,16 @@ impl Plugin for BevyKayakUIExtractPlugin { pub fn extract( mut commands: Commands, - context: Extract<Option<Res<BevyContext>>>, + context: Extract<Res<Context>>, fonts: Extract<Res<Assets<KayakFont>>>, font_mapping: Extract<Res<FontMapping>>, - image_manager: Extract<Res<ImageManager>>, + node_query: Extract<Query<&Node>>, images: Extract<Res<Assets<Image>>>, windows: Extract<Res<Windows>>, ) { - if context.is_none() { - return; - } - - let context = context.as_ref().unwrap(); - - let render_primitives = if let Ok(context) = context.kayak_context.read() { - context.widget_manager.build_render_primitives() - } else { - vec![] - }; - - // dbg!(&render_primitives); + // dbg!("STARTED"); + let render_primitives = context.build_render_primitives(&node_query); + // dbg!("FINISHED"); let dpi = if let Some(window) = windows.get_primary() { window.scale_factor() as f32 @@ -60,6 +44,8 @@ pub fn extract( 1.0 }; + // dbg!(&render_primitives); + let mut extracted_quads = Vec::new(); for render_primitive in render_primitives { match render_primitive { @@ -68,25 +54,21 @@ pub fn extract( extracted_quads.extend(text_quads); } RenderPrimitive::Image { .. } => { - let image_quads = image::extract_images(&render_primitive, &image_manager, dpi); + let image_quads = image::extract_images(&render_primitive, dpi); extracted_quads.extend(image_quads); } RenderPrimitive::Quad { .. } => { - let quad_quads = quad::extract_quads(&render_primitive, 1.0); + let quad_quads = super::quad::extract_quads(&render_primitive, 1.0); extracted_quads.extend(quad_quads); } RenderPrimitive::NinePatch { .. } => { let nine_patch_quads = - nine_patch::extract_nine_patch(&render_primitive, &image_manager, &images, dpi); + nine_patch::extract_nine_patch(&render_primitive, &images, dpi); extracted_quads.extend(nine_patch_quads); } RenderPrimitive::TextureAtlas { .. } => { - let texture_atlas_quads = texture_atlas::extract_texture_atlas( - &render_primitive, - &image_manager, - &images, - dpi, - ); + let texture_atlas_quads = + texture_atlas::extract_texture_atlas(&render_primitive, &images, dpi); extracted_quads.extend(texture_atlas_quads); } RenderPrimitive::Clip { layout } => { @@ -115,5 +97,6 @@ pub fn extract( } } + // dbg!(&extracted_quads); commands.spawn_batch(extracted_quads); } diff --git a/bevy_kayak_ui/src/render/font/extract.rs b/src/render/font/extract.rs similarity index 88% rename from bevy_kayak_ui/src/render/font/extract.rs rename to src/render/font/extract.rs index 569f5e6..ffbd8ea 100644 --- a/bevy_kayak_ui/src/render/font/extract.rs +++ b/src/render/font/extract.rs @@ -1,15 +1,13 @@ use bevy::{ math::Vec2, - prelude::{Assets, Res}, - sprite::Rect, + prelude::{Assets, Rect, Res}, }; -use kayak_core::render_primitive::RenderPrimitive; use kayak_font::KayakFont; -use crate::to_bevy_color; -use bevy_kayak_renderer::{ +use crate::{ render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, + render_primitive::RenderPrimitive, + styles::Corner, }; use super::font_mapping::FontMapping; @@ -36,7 +34,9 @@ pub fn extract_texts( let font_handle = font_mapping.get_handle(font.clone()).unwrap(); let font = match fonts.get(&font_handle) { Some(font) => font, - None => return Vec::new(), + None => { + return Vec::new(); + } }; let base_position = Vec2::new(layout.posx, layout.posy + properties.font_size); @@ -54,7 +54,7 @@ pub fn extract_texts( min: position, max: position + size, }, - color: to_bevy_color(background_color), + color: *background_color, vertex_index: 0, char_id: font.get_char_id(glyph_rect.content).unwrap(), z_index: layout.z_index, diff --git a/bevy_kayak_ui/src/render/font/font_mapping.rs b/src/render/font/font_mapping.rs similarity index 70% rename from bevy_kayak_ui/src/render/font/font_mapping.rs rename to src/render/font/font_mapping.rs index 1235198..786fccc 100644 --- a/bevy_kayak_ui/src/render/font/font_mapping.rs +++ b/src/render/font/font_mapping.rs @@ -1,106 +1,107 @@ -use bevy::{ - prelude::{Assets, Handle, Res}, - utils::HashMap, -}; -use kayak_font::KayakFont; - -use crate::BevyContext; - -/// A resource used to manage fonts for use in a `KayakContext` -/// -/// # Example -/// -/// ``` -/// use bevy::prelude::*; -/// use bevy_kayak_ui::FontMapping; -/// -/// fn setup_ui( -/// # mut commands: Commands, -/// asset_server: Res<AssetServer>, -/// mut font_mapping: ResMut<FontMapping> -/// ) { -/// # commands.spawn_bundle(UICameraBundle::new()); -/// # -/// font_mapping.set_default(asset_server.load("roboto.kayak_font")); -/// // ... -/// # -/// # let context = BevyContext::new(|context| { -/// # render! { -/// # <App> -/// # <Text content={"Hello World!".to_string()} /> -/// # </App> -/// # } -/// # }); -/// # -/// # commands.insert_resource(context); -/// } -/// ``` -pub struct FontMapping { - font_ids: HashMap<Handle<KayakFont>, String>, - font_handles: HashMap<String, Handle<KayakFont>>, - new_fonts: Vec<String>, -} - -impl Default for FontMapping { - fn default() -> Self { - Self { - font_ids: HashMap::default(), - font_handles: HashMap::default(), - new_fonts: Vec::new(), - } - } -} - -impl FontMapping { - /// Add a `KayakFont` to be tracked - pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) { - let key = key.into(); - if !self.font_ids.contains_key(&handle) { - self.font_ids.insert(handle.clone(), key.clone()); - self.new_fonts.push(key.clone()); - self.font_handles.insert(key, handle); - } - } - - /// Set a default `KayakFont` - pub fn set_default(&mut self, handle: Handle<KayakFont>) { - self.add(kayak_core::DEFAULT_FONT, handle); - } - - pub(crate) fn mark_all_as_new(&mut self) { - self.new_fonts - .extend(self.font_handles.keys().map(|key| key.clone())); - } - - /// Get the handle for the given font name - pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> { - self.font_handles - .get(&id) - .and_then(|item| Some(item.clone())) - } - - /// Get the font name for the given handle - pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> { - self.font_ids - .get(font) - .and_then(|font_id| Some(font_id.clone())) - } - - pub(crate) fn add_loaded_to_kayak( - &mut self, - fonts: &Res<Assets<KayakFont>>, - context: &BevyContext, - ) { - if let Ok(mut kayak_context) = context.kayak_context.write() { - let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>(); - for font_key in new_fonts { - let font_handle = self.font_handles.get(&font_key).unwrap(); - if let Some(font) = fonts.get(font_handle) { - kayak_context.set_asset(font_key, font.clone()); - } else { - self.new_fonts.push(font_key); - } - } - } - } -} +use bevy::{ + prelude::{Handle, Resource}, + utils::HashMap, +}; +use kayak_font::KayakFont; + +// use crate::context::Context; + +/// A resource used to manage fonts for use in a `KayakContext` +/// +/// # Example +/// +/// ``` +/// use bevy::prelude::*; +/// use bevy_kayak_ui::FontMapping; +/// +/// fn setup_ui( +/// # mut commands: Commands, +/// asset_server: Res<AssetServer>, +/// mut font_mapping: ResMut<FontMapping> +/// ) { +/// # commands.spawn_bundle(UICameraBundle::new()); +/// # +/// font_mapping.set_default(asset_server.load("roboto.kayak_font")); +/// // ... +/// # +/// # let context = BevyContext::new(|context| { +/// # render! { +/// # <App> +/// # <Text content={"Hello World!".to_string()} /> +/// # </App> +/// # } +/// # }); +/// # +/// # commands.insert_resource(context); +/// } +/// ``` +#[derive(Resource)] +pub struct FontMapping { + font_ids: HashMap<Handle<KayakFont>, String>, + font_handles: HashMap<String, Handle<KayakFont>>, + new_fonts: Vec<String>, +} + +impl Default for FontMapping { + fn default() -> Self { + Self { + font_ids: HashMap::default(), + font_handles: HashMap::default(), + new_fonts: Vec::new(), + } + } +} + +impl FontMapping { + /// Add a `KayakFont` to be tracked + pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) { + let key = key.into(); + if !self.font_ids.contains_key(&handle) { + self.font_ids.insert(handle.clone(), key.clone()); + self.new_fonts.push(key.clone()); + self.font_handles.insert(key, handle); + } + } + + /// Set a default `KayakFont` + pub fn set_default(&mut self, handle: Handle<KayakFont>) { + self.add(crate::DEFAULT_FONT, handle); + } + + pub(crate) fn mark_all_as_new(&mut self) { + self.new_fonts + .extend(self.font_handles.keys().map(|key| key.clone())); + } + + /// Get the handle for the given font name + pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> { + self.font_handles + .get(&id) + .and_then(|item| Some(item.clone())) + } + + /// Get the font name for the given handle + pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> { + self.font_ids + .get(font) + .and_then(|font_id| Some(font_id.clone())) + } + + // pub(crate) fn add_loaded_to_kayak( + // &mut self, + // fonts: &Res<Assets<KayakFont>>, + // context: &Context, + // ) { + // if let Ok(mut kayak_context) = context.kayak_context.write() { + // let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>(); + // for font_key in new_fonts { + // let font_handle = self.font_handles.get(&font_key).unwrap(); + // if let Some(font) = fonts.get(font_handle) { + // kayak_context.set_asset(font_key, font.clone()); + // } else { + // self.new_fonts.push(font_key); + // } + // } + // } + // } +} diff --git a/bevy_kayak_ui/src/render/font/mod.rs b/src/render/font/mod.rs similarity index 61% rename from bevy_kayak_ui/src/render/font/mod.rs rename to src/render/font/mod.rs index f70809f..55a330d 100644 --- a/bevy_kayak_ui/src/render/font/mod.rs +++ b/src/render/font/mod.rs @@ -4,11 +4,11 @@ use kayak_font::KayakFont; mod extract; mod font_mapping; -use crate::BevyContext; - pub use extract::extract_texts; pub use font_mapping::*; +use crate::context::Context; + #[derive(Default)] pub struct TextRendererPlugin; @@ -21,13 +21,13 @@ impl Plugin for TextRendererPlugin { fn process_loaded_fonts( mut font_mapping: ResMut<FontMapping>, - fonts: Res<Assets<KayakFont>>, - bevy_context: Option<Res<BevyContext>>, + _fonts: Res<Assets<KayakFont>>, + context_resource: Res<Context>, ) { - if let Some(context) = bevy_context { - if context.is_added() { - font_mapping.mark_all_as_new(); - } - font_mapping.add_loaded_to_kayak(&fonts, &context); + // if let Some(context = context_resource.as_ref() { + if context_resource.is_added() { + font_mapping.mark_all_as_new(); } + // font_mapping.add_loaded_to_kayak(&fonts, &context); + // } } diff --git a/bevy_kayak_ui/src/render/image/extract.rs b/src/render/image/extract.rs similarity index 73% rename from bevy_kayak_ui/src/render/image/extract.rs rename to src/render/image/extract.rs index 28fca83..7c41fe1 100644 --- a/bevy_kayak_ui/src/render/image/extract.rs +++ b/src/render/image/extract.rs @@ -1,17 +1,11 @@ -use bevy::{math::Vec2, prelude::Res, render::color::Color, sprite::Rect}; -use kayak_core::render_primitive::RenderPrimitive; - -use crate::ImageManager; -use bevy_kayak_renderer::{ +use crate::{ render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, + render_primitive::RenderPrimitive, + styles::Corner, }; +use bevy::{math::Vec2, prelude::Rect, render::color::Color}; -pub fn extract_images( - render_command: &RenderPrimitive, - image_manager: &Res<ImageManager>, - dpi: f32, -) -> Vec<ExtractQuadBundle> { +pub fn extract_images(render_command: &RenderPrimitive, dpi: f32) -> Vec<ExtractQuadBundle> { let (border_radius, layout, handle) = match render_command { RenderPrimitive::Image { border_radius, @@ -40,9 +34,7 @@ pub fn extract_images( bottom_left: border_radius.bottom_left, bottom_right: border_radius.bottom_right, }, - image: image_manager - .get_handle(handle) - .and_then(|a| Some(a.clone_weak())), + image: Some(handle.clone_weak()), uv_max: None, uv_min: None, }, diff --git a/src/render/image/mod.rs b/src/render/image/mod.rs new file mode 100644 index 0000000..fbdcb80 --- /dev/null +++ b/src/render/image/mod.rs @@ -0,0 +1,2 @@ +mod extract; +pub use extract::extract_images; diff --git a/bevy_kayak_renderer/src/render/mod.rs b/src/render/mod.rs similarity index 86% rename from bevy_kayak_renderer/src/render/mod.rs rename to src/render/mod.rs index c75adb5..cd66d6c 100644 --- a/bevy_kayak_renderer/src/render/mod.rs +++ b/src/render/mod.rs @@ -12,8 +12,14 @@ use crate::{ CameraUiKayak, }; -use self::ui_pass::TransparentUI; +use self::{extract::BevyKayakUIExtractPlugin, ui_pass::TransparentUI}; +mod extract; +pub(crate) mod font; +pub(crate) mod image; +pub(crate) mod nine_patch; +pub(crate) mod quad; +pub(crate) mod texture_atlas; mod ui_pass; pub mod unified; @@ -58,7 +64,9 @@ impl Plugin for BevyKayakUIRenderPlugin { // graph.add_node_edge(MAIN_PASS, draw_ui_graph::NAME).unwrap(); - app.add_plugin(UnifiedRenderPlugin); + app.add_plugin(font::TextRendererPlugin) + .add_plugin(UnifiedRenderPlugin) + .add_plugin(BevyKayakUIExtractPlugin); } } diff --git a/bevy_kayak_ui/src/render/nine_patch/extract.rs b/src/render/nine_patch/extract.rs similarity index 95% rename from bevy_kayak_ui/src/render/nine_patch/extract.rs rename to src/render/nine_patch/extract.rs index 1ed3782..e2d1271 100644 --- a/bevy_kayak_ui/src/render/nine_patch/extract.rs +++ b/src/render/nine_patch/extract.rs @@ -1,19 +1,16 @@ -use crate::ImageManager; +use crate::{ + render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, + render_primitive::RenderPrimitive, + styles::Corner, +}; use bevy::{ math::Vec2, - prelude::{Assets, Res}, + prelude::{Assets, Rect, Res}, render::{color::Color, texture::Image}, - sprite::Rect, }; -use bevy_kayak_renderer::{ - render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, -}; -use kayak_core::render_primitive::RenderPrimitive; pub fn extract_nine_patch( render_primitive: &RenderPrimitive, - image_manager: &Res<ImageManager>, images: &Res<Assets<Image>>, dpi: f32, ) -> Vec<ExtractQuadBundle> { @@ -28,11 +25,7 @@ pub fn extract_nine_patch( _ => panic!(""), }; - let image_handle = image_manager - .get_handle(handle) - .and_then(|a| Some(a.clone_weak())); - - let image = images.get(image_handle.as_ref().unwrap()); + let image = images.get(handle); if image.is_none() { return vec![]; @@ -61,7 +54,7 @@ pub fn extract_nine_patch( quad_type: UIQuadType::Image, type_index: 0, border_radius: Corner::default(), - image: image_handle, + image: Some(handle.clone_weak()), uv_max: None, uv_min: None, }; diff --git a/bevy_kayak_ui/src/render/nine_patch/mod.rs b/src/render/nine_patch/mod.rs similarity index 96% rename from bevy_kayak_ui/src/render/nine_patch/mod.rs rename to src/render/nine_patch/mod.rs index cf48787..5450e10 100644 --- a/bevy_kayak_ui/src/render/nine_patch/mod.rs +++ b/src/render/nine_patch/mod.rs @@ -1,2 +1,2 @@ -mod extract; -pub use extract::extract_nine_patch; +mod extract; +pub use extract::extract_nine_patch; diff --git a/bevy_kayak_ui/src/render/quad/extract.rs b/src/render/quad/extract.rs similarity index 91% rename from bevy_kayak_ui/src/render/quad/extract.rs rename to src/render/quad/extract.rs index 52b2e8a..9f5f622 100644 --- a/bevy_kayak_ui/src/render/quad/extract.rs +++ b/src/render/quad/extract.rs @@ -1,10 +1,9 @@ -use crate::to_bevy_color; -use bevy::{math::Vec2, sprite::Rect}; -use bevy_kayak_renderer::{ +use crate::{ render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, + render_primitive::RenderPrimitive, + styles::Corner, }; -use kayak_core::render_primitive::RenderPrimitive; +use bevy::{math::Vec2, prelude::Rect}; pub fn extract_quads(render_primitive: &RenderPrimitive, dpi: f32) -> Vec<ExtractQuadBundle> { let (background_color, border_color, layout, border_radius, mut border) = match render_primitive @@ -38,7 +37,7 @@ pub fn extract_quads(render_primitive: &RenderPrimitive, dpi: f32) -> Vec<Extrac layout.posy + (layout.height * dpi), ), }, - color: to_bevy_color(&border_color), + color: border_color, vertex_index: 0, char_id: 0, z_index: layout.z_index, @@ -65,7 +64,7 @@ pub fn extract_quads(render_primitive: &RenderPrimitive, dpi: f32) -> Vec<Extrac (layout.posy + (layout.height * dpi)) - border.bottom, ), }, - color: to_bevy_color(&background_color), + color: background_color, vertex_index: 0, char_id: 0, z_index: layout.z_index, diff --git a/bevy_kayak_ui/src/render/quad/mod.rs b/src/render/quad/mod.rs similarity index 95% rename from bevy_kayak_ui/src/render/quad/mod.rs rename to src/render/quad/mod.rs index c9421c2..69dee97 100644 --- a/bevy_kayak_ui/src/render/quad/mod.rs +++ b/src/render/quad/mod.rs @@ -1,2 +1,2 @@ -mod extract; -pub use extract::extract_quads; +mod extract; +pub use extract::extract_quads; diff --git a/bevy_kayak_ui/src/render/texture_atlas/extract.rs b/src/render/texture_atlas/extract.rs similarity index 72% rename from bevy_kayak_ui/src/render/texture_atlas/extract.rs rename to src/render/texture_atlas/extract.rs index 4aebae9..41eca02 100644 --- a/bevy_kayak_ui/src/render/texture_atlas/extract.rs +++ b/src/render/texture_atlas/extract.rs @@ -1,19 +1,16 @@ -use crate::ImageManager; +use crate::{ + render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, + render_primitive::RenderPrimitive, + styles::Corner, +}; use bevy::{ math::Vec2, - prelude::{Assets, Res}, + prelude::{Assets, Rect, Res}, render::{color::Color, texture::Image}, - sprite::Rect, }; -use bevy_kayak_renderer::{ - render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, - Corner, -}; -use kayak_core::render_primitive::RenderPrimitive; pub fn extract_texture_atlas( render_primitive: &RenderPrimitive, - image_manager: &Res<ImageManager>, images: &Res<Assets<Image>>, dpi: f32, ) -> Vec<ExtractQuadBundle> { @@ -29,11 +26,7 @@ pub fn extract_texture_atlas( _ => panic!(""), }; - let image_handle = image_manager - .get_handle(handle) - .and_then(|a| Some(a.clone_weak())); - - let image = images.get(image_handle.as_ref().unwrap()); + let image = images.get(&handle); if image.is_none() { return vec![]; @@ -56,12 +49,12 @@ pub fn extract_texture_atlas( max: Vec2::new(layout.posx + layout.width, layout.posy + layout.height), }, uv_min: Some(Vec2::new( - position.0 / image_size.x, - 1.0 - ((position.1 + size.1) / image_size.y), + position.x / image_size.x, + 1.0 - ((position.y + size.y) / image_size.y), )), uv_max: Some(Vec2::new( - (position.0 + size.0) / image_size.x, - 1.0 - (position.1 / image_size.y), + (position.x + size.x) / image_size.x, + 1.0 - (position.y / image_size.y), )), color: Color::WHITE, vertex_index: 0, @@ -71,7 +64,7 @@ pub fn extract_texture_atlas( quad_type: UIQuadType::Image, type_index: 0, border_radius: Corner::default(), - image: image_handle, + image: Some(handle.clone_weak()), }, }; extracted_quads.push(quad); diff --git a/bevy_kayak_ui/src/render/texture_atlas/mod.rs b/src/render/texture_atlas/mod.rs similarity index 96% rename from bevy_kayak_ui/src/render/texture_atlas/mod.rs rename to src/render/texture_atlas/mod.rs index bd5825a..a9d1ed8 100644 --- a/bevy_kayak_ui/src/render/texture_atlas/mod.rs +++ b/src/render/texture_atlas/mod.rs @@ -1,2 +1,2 @@ -mod extract; -pub use extract::extract_texture_atlas; +mod extract; +pub use extract::extract_texture_atlas; diff --git a/bevy_kayak_renderer/src/render/ui_pass.rs b/src/render/ui_pass.rs similarity index 96% rename from bevy_kayak_renderer/src/render/ui_pass.rs rename to src/render/ui_pass.rs index 13a5c8d..bc9ff4f 100644 --- a/bevy_kayak_renderer/src/render/ui_pass.rs +++ b/src/render/ui_pass.rs @@ -1,103 +1,103 @@ -use bevy::ecs::prelude::*; -use bevy::render::render_phase::{DrawFunctionId, PhaseItem}; -use bevy::render::render_resource::{CachedRenderPipelineId, RenderPassColorAttachment}; -use bevy::render::{ - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, - render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, - render_resource::{LoadOp, Operations, RenderPassDescriptor}, - renderer::RenderContext, - view::{ExtractedView, ViewTarget}, -}; -use bevy::utils::FloatOrd; - -pub struct TransparentUI { - pub sort_key: FloatOrd, - pub entity: Entity, - pub pipeline: CachedRenderPipelineId, - pub draw_function: DrawFunctionId, -} - -impl PhaseItem for TransparentUI { - type SortKey = FloatOrd; - - #[inline] - fn sort_key(&self) -> Self::SortKey { - self.sort_key - } - - #[inline] - fn draw_function(&self) -> DrawFunctionId { - self.draw_function - } -} - -pub struct MainPassUINode { - query: - QueryState<(&'static RenderPhase<TransparentUI>, &'static ViewTarget), With<ExtractedView>>, -} - -impl MainPassUINode { - pub const IN_VIEW: &'static str = "view"; - - pub fn new(world: &mut World) -> Self { - Self { - query: QueryState::new(world), - } - } -} - -impl Node for MainPassUINode { - fn input(&self) -> Vec<SlotInfo> { - vec![SlotInfo::new(MainPassUINode::IN_VIEW, SlotType::Entity)] - } - - fn update(&mut self, world: &mut World) { - self.query.update_archetypes(world); - } - - fn run( - &self, - graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, - ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - // adapted from bevy itself; - // see: <https://github.com/bevyengine/bevy/commit/09a3d8abe062984479bf0e99fcc1508bb722baf6> - let (transparent_phase, target) = match self.query.get_manual(world, view_entity) { - Ok(it) => it, - _ => return Ok(()), - }; - // let clear_color = world.get_resource::<ClearColor>().unwrap(); - { - let pass_descriptor = RenderPassDescriptor { - label: Some("main_transparent_pass_UI"), - color_attachments: &[Some(RenderPassColorAttachment { - view: &target.view, - resolve_target: None, - ops: Operations { - load: LoadOp::Load, //Clear(clear_color.0.into()), - store: true, - }, - })], - depth_stencil_attachment: None, - }; - - let draw_functions = world - .get_resource::<DrawFunctions<TransparentUI>>() - .unwrap(); - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut draw_functions = draw_functions.write(); - let mut tracked_pass = TrackedRenderPass::new(render_pass); - for item in transparent_phase.items.iter() { - let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); - draw_function.draw(world, &mut tracked_pass, view_entity, item); - } - } - - Ok(()) - } -} +use bevy::ecs::prelude::*; +use bevy::render::render_phase::{DrawFunctionId, PhaseItem}; +use bevy::render::render_resource::{CachedRenderPipelineId, RenderPassColorAttachment}; +use bevy::render::{ + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, + render_resource::{LoadOp, Operations, RenderPassDescriptor}, + renderer::RenderContext, + view::{ExtractedView, ViewTarget}, +}; +use bevy::utils::FloatOrd; + +pub struct TransparentUI { + pub sort_key: FloatOrd, + pub entity: Entity, + pub pipeline: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for TransparentUI { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } +} + +pub struct MainPassUINode { + query: + QueryState<(&'static RenderPhase<TransparentUI>, &'static ViewTarget), With<ExtractedView>>, +} + +impl MainPassUINode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for MainPassUINode { + fn input(&self) -> Vec<SlotInfo> { + vec![SlotInfo::new(MainPassUINode::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + // adapted from bevy itself; + // see: <https://github.com/bevyengine/bevy/commit/09a3d8abe062984479bf0e99fcc1508bb722baf6> + let (transparent_phase, target) = match self.query.get_manual(world, view_entity) { + Ok(it) => it, + _ => return Ok(()), + }; + // let clear_color = world.get_resource::<ClearColor>().unwrap(); + { + let pass_descriptor = RenderPassDescriptor { + label: Some("main_transparent_pass_UI"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &target.view, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, //Clear(clear_color.0.into()), + store: true, + }, + })], + depth_stencil_attachment: None, + }; + + let draw_functions = world + .get_resource::<DrawFunctions<TransparentUI>>() + .unwrap(); + + let render_pass = render_context + .command_encoder + .begin_render_pass(&pass_descriptor); + let mut draw_functions = draw_functions.write(); + let mut tracked_pass = TrackedRenderPass::new(render_pass); + for item in transparent_phase.items.iter() { + let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); + draw_function.draw(world, &mut tracked_pass, view_entity, item); + } + } + + Ok(()) + } +} diff --git a/bevy_kayak_renderer/src/render/unified/mod.rs b/src/render/unified/mod.rs similarity index 94% rename from bevy_kayak_renderer/src/render/unified/mod.rs rename to src/render/unified/mod.rs index 1283d64..e62e670 100644 --- a/bevy_kayak_renderer/src/render/unified/mod.rs +++ b/src/render/unified/mod.rs @@ -1,5 +1,5 @@ use bevy::{ - prelude::{Assets, Commands, HandleUntyped, Plugin, Res}, + prelude::{Assets, Commands, HandleUntyped, Plugin, Res, Resource}, reflect::TypeUuid, render::{ render_phase::DrawFunctions, render_resource::Shader, Extract, RenderApp, RenderStage, @@ -18,7 +18,7 @@ use crate::{ use self::pipeline::ImageBindGroups; pub mod pipeline; -mod text; +pub mod text; pub const UNIFIED_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450); @@ -27,12 +27,12 @@ pub struct UnifiedRenderPlugin; impl Plugin for UnifiedRenderPlugin { fn build(&self, app: &mut bevy::prelude::App) { + app.add_plugin(text::TextRendererPlugin); + let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap(); let unified_shader = Shader::from_wgsl(include_str!("shader.wgsl")); shaders.set_untracked(UNIFIED_SHADER_HANDLE, unified_shader); - app.add_plugin(text::TextRendererPlugin); - let render_app = app.sub_app_mut(RenderApp); render_app .init_resource::<ImageBindGroups>() @@ -53,6 +53,7 @@ impl Plugin for UnifiedRenderPlugin { } } +#[derive(Resource)] pub struct Dpi(f32); pub fn extract_baseline( diff --git a/bevy_kayak_renderer/src/render/unified/pipeline.rs b/src/render/unified/pipeline.rs similarity index 99% rename from bevy_kayak_renderer/src/render/unified/pipeline.rs rename to src/render/unified/pipeline.rs index d73a940..570c3e7 100644 --- a/bevy_kayak_renderer/src/render/unified/pipeline.rs +++ b/src/render/unified/pipeline.rs @@ -1,3 +1,4 @@ +use bevy::prelude::{Rect, Resource}; use bevy::render::render_resource::{DynamicUniformBuffer, ShaderType}; use bevy::utils::FloatOrd; use bevy::{ @@ -27,7 +28,6 @@ use bevy::{ texture::{BevyDefault, GpuImage, Image}, view::{ViewUniformOffset, ViewUniforms}, }, - sprite::Rect, utils::HashMap, }; use bytemuck::{Pod, Zeroable}; @@ -37,9 +37,11 @@ use kayak_font::{ }; use super::{Dpi, UNIFIED_SHADER_HANDLE}; +use crate::prelude::Corner; use crate::render::ui_pass::TransparentUI; -use crate::{Corner, WindowSize}; +use crate::WindowSize; +#[derive(Resource)] pub struct UnifiedPipeline { view_layout: BindGroupLayout, types_layout: BindGroupLayout, @@ -342,6 +344,7 @@ struct QuadType { pub _padding_3: i32, } +#[derive(Resource)] pub struct QuadMeta { vertices: BufferVec<QuadVertex>, view_bind_group: Option<BindGroup>, @@ -360,7 +363,7 @@ impl Default for QuadMeta { } } -#[derive(Default)] +#[derive(Default, Resource)] pub struct ImageBindGroups { values: HashMap<Handle<Image>, BindGroup>, } diff --git a/bevy_kayak_renderer/src/render/unified/shader.wgsl b/src/render/unified/shader.wgsl similarity index 96% rename from bevy_kayak_renderer/src/render/unified/shader.wgsl rename to src/render/unified/shader.wgsl index 5a8fcf9..e299a44 100644 --- a/bevy_kayak_renderer/src/render/unified/shader.wgsl +++ b/src/render/unified/shader.wgsl @@ -1,101 +1,101 @@ -struct View { - view_proj: mat4x4<f32>, - world_position: vec3<f32>, -}; -@group(0) @binding(0) -var<uniform> view: View; - -struct QuadType { - t: i32, - _padding_1: i32, - _padding_2: i32, - _padding_3: i32, -}; - -@group(2) @binding(0) -var<uniform> quad_type: QuadType; - -struct VertexOutput { - @builtin(position) position: vec4<f32>, - @location(0) color: vec4<f32>, - @location(1) uv: vec3<f32>, - @location(2) pos: vec2<f32>, - @location(3) size: vec2<f32>, - @location(4) border_radius: f32, - @location(5) pixel_position: vec2<f32>, -}; - -@vertex -fn vertex( - @location(0) vertex_position: vec3<f32>, - @location(1) vertex_color: vec4<f32>, - @location(2) vertex_uv: vec4<f32>, - @location(3) vertex_pos_size: vec4<f32>, -) -> VertexOutput { - var out: VertexOutput; - out.color = vertex_color; - out.pos = (vertex_position.xy - vertex_pos_size.xy); - out.position = view.view_proj * vec4<f32>(vertex_position, 1.0); - out.pixel_position = out.position.xy; - out.uv = vertex_uv.xyz; - out.size = vertex_pos_size.zw; - out.border_radius = vertex_uv.w; - return out; -} - -@group(1) @binding(0) -var font_texture: texture_2d_array<f32>; -@group(1) @binding(1) -var font_sampler: sampler; - -@group(3) @binding(0) -var image_texture: texture_2d<f32>; -@group(3) @binding(1) -var image_sampler: sampler; - -let RADIUS: f32 = 0.1; - -// Where P is the position in pixel space, B is the size of the box adn R is the radius of the current corner. -fn sdRoundBox(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 { - var q = abs(p) - b + r; - return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r; -} - -@fragment -fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { - if (quad_type.t == 0) { - var size = in.size; - var pos = in.pos.xy * 2.0; - // Lock border to max size. This is similar to how HTML/CSS handles border radius. - var bs = min(in.border_radius * 2.0, min(size.x, size.y)); - var rect_dist = sdRoundBox( - pos - size, - size, - bs, - ); - rect_dist = 1.0 - smoothstep(0.0, fwidth(rect_dist), rect_dist); - return vec4<f32>(in.color.rgb, rect_dist * in.color.a); - } - if (quad_type.t == 1) { - var px_range = 3.5; - var tex_dimensions = textureDimensions(font_texture); - var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y)); - var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z)); - var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b)); - var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy)); - var a = clamp(sig_dist + 0.5, 0.0, 1.0); - return vec4<f32>(in.color.rgb, a); - } - if (quad_type.t == 2) { - var bs = min(in.border_radius, min(in.size.x, in.size.y)); - var mask = sdRoundBox( - in.pos.xy * 2.0 - (in.size.xy), - in.size.xy, - bs, - ); - mask = 1.0 - smoothstep(0.0, fwidth(mask), mask); - var color = textureSample(image_texture, image_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y)); - return vec4<f32>(color.rgb * in.color.rgb, color.a * in.color.a * mask); - } - return in.color; -} +struct View { + view_proj: mat4x4<f32>, + world_position: vec3<f32>, +}; +@group(0) @binding(0) +var<uniform> view: View; + +struct QuadType { + t: i32, + _padding_1: i32, + _padding_2: i32, + _padding_3: i32, +}; + +@group(2) @binding(0) +var<uniform> quad_type: QuadType; + +struct VertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) color: vec4<f32>, + @location(1) uv: vec3<f32>, + @location(2) pos: vec2<f32>, + @location(3) size: vec2<f32>, + @location(4) border_radius: f32, + @location(5) pixel_position: vec2<f32>, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3<f32>, + @location(1) vertex_color: vec4<f32>, + @location(2) vertex_uv: vec4<f32>, + @location(3) vertex_pos_size: vec4<f32>, +) -> VertexOutput { + var out: VertexOutput; + out.color = vertex_color; + out.pos = (vertex_position.xy - vertex_pos_size.xy); + out.position = view.view_proj * vec4<f32>(vertex_position, 1.0); + out.pixel_position = out.position.xy; + out.uv = vertex_uv.xyz; + out.size = vertex_pos_size.zw; + out.border_radius = vertex_uv.w; + return out; +} + +@group(1) @binding(0) +var font_texture: texture_2d_array<f32>; +@group(1) @binding(1) +var font_sampler: sampler; + +@group(3) @binding(0) +var image_texture: texture_2d<f32>; +@group(3) @binding(1) +var image_sampler: sampler; + +let RADIUS: f32 = 0.1; + +// Where P is the position in pixel space, B is the size of the box adn R is the radius of the current corner. +fn sdRoundBox(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 { + var q = abs(p) - b + r; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r; +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { + if (quad_type.t == 0) { + var size = in.size; + var pos = in.pos.xy * 2.0; + // Lock border to max size. This is similar to how HTML/CSS handles border radius. + var bs = min(in.border_radius * 2.0, min(size.x, size.y)); + var rect_dist = sdRoundBox( + pos - size, + size, + bs, + ); + rect_dist = 1.0 - smoothstep(0.0, fwidth(rect_dist), rect_dist); + return vec4<f32>(in.color.rgb, rect_dist * in.color.a); + } + if (quad_type.t == 1) { + var px_range = 3.5; + var tex_dimensions = textureDimensions(font_texture); + var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y)); + var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z)); + var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b)); + var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy)); + var a = clamp(sig_dist + 0.5, 0.0, 1.0); + return vec4<f32>(in.color.rgb, a); + } + if (quad_type.t == 2) { + var bs = min(in.border_radius, min(in.size.x, in.size.y)); + var mask = sdRoundBox( + in.pos.xy * 2.0 - (in.size.xy), + in.size.xy, + bs, + ); + mask = 1.0 - smoothstep(0.0, fwidth(mask), mask); + var color = textureSample(image_texture, image_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y)); + return vec4<f32>(color.rgb * in.color.rgb, color.a * in.color.a * mask); + } + return in.color; +} diff --git a/bevy_kayak_renderer/src/render/unified/text.rs b/src/render/unified/text.rs similarity index 96% rename from bevy_kayak_renderer/src/render/unified/text.rs rename to src/render/unified/text.rs index 7b31d87..400917b 100644 --- a/bevy_kayak_renderer/src/render/unified/text.rs +++ b/src/render/unified/text.rs @@ -1,33 +1,33 @@ -use bevy::{ - prelude::{Plugin, Res, ResMut}, - render::{ - render_asset::RenderAssets, - renderer::{RenderDevice, RenderQueue}, - texture::Image, - RenderApp, RenderStage, - }, -}; -use kayak_font::bevy::{FontTextureCache, KayakFontPlugin}; - -use super::pipeline::UnifiedPipeline; - -#[derive(Default)] -pub struct TextRendererPlugin; - -impl Plugin for TextRendererPlugin { - fn build(&self, app: &mut bevy::prelude::App) { - app.add_plugin(KayakFontPlugin); - - let render_app = app.sub_app_mut(RenderApp); - render_app.add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture); - } -} -fn create_and_update_font_cache_texture( - device: Res<RenderDevice>, - queue: Res<RenderQueue>, - pipeline: Res<UnifiedPipeline>, - mut font_texture_cache: ResMut<FontTextureCache>, - images: Res<RenderAssets<Image>>, -) { - font_texture_cache.process_new(&device, &queue, pipeline.into_inner(), &images); -} +use bevy::{ + prelude::{Plugin, Res, ResMut}, + render::{ + render_asset::RenderAssets, + renderer::{RenderDevice, RenderQueue}, + texture::Image, + RenderApp, RenderStage, + }, +}; +use kayak_font::bevy::{FontTextureCache, KayakFontPlugin}; + +use super::pipeline::UnifiedPipeline; + +#[derive(Default)] +pub struct TextRendererPlugin; + +impl Plugin for TextRendererPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_plugin(KayakFontPlugin); + + let render_app = app.sub_app_mut(RenderApp); + render_app.add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture); + } +} +fn create_and_update_font_cache_texture( + device: Res<RenderDevice>, + queue: Res<RenderQueue>, + pipeline: Res<UnifiedPipeline>, + mut font_texture_cache: ResMut<FontTextureCache>, + images: Res<RenderAssets<Image>>, +) { + font_texture_cache.process_new(&device, &queue, pipeline.into_inner(), &images); +} diff --git a/kayak_core/src/render_primitive.rs b/src/render_primitive.rs similarity index 82% rename from kayak_core/src/render_primitive.rs rename to src/render_primitive.rs index 51e7431..9e11073 100644 --- a/kayak_core/src/render_primitive.rs +++ b/src/render_primitive.rs @@ -1,9 +1,8 @@ use crate::{ - color::Color, - layout_cache::Rect, - render_command::RenderCommand, - styles::{Corner, Edge, Style}, + layout::Rect, + styles::{Corner, Edge, KStyle, RenderCommand}, }; +use bevy::prelude::{Color, Handle, Image, Vec2}; use kayak_font::{TextLayout, TextProperties}; #[derive(Debug, Clone, PartialEq)] @@ -30,18 +29,18 @@ pub enum RenderPrimitive { Image { border_radius: Corner<f32>, layout: Rect, - handle: u16, + handle: Handle<Image>, }, TextureAtlas { - size: (f32, f32), - position: (f32, f32), + size: Vec2, + position: Vec2, layout: Rect, - handle: u16, + handle: Handle<Image>, }, NinePatch { border: Edge<f32>, layout: Rect, - handle: u16, + handle: Handle<Image>, }, } @@ -59,13 +58,17 @@ impl RenderPrimitive { } } -impl From<&Style> for RenderPrimitive { - fn from(style: &Style) -> Self { +impl From<&KStyle> for RenderPrimitive { + fn from(style: &KStyle) -> Self { let render_command = style.render_command.resolve(); - let background_color = style.background_color.resolve_or(Color::TRANSPARENT); + let background_color = style + .background_color + .resolve_or(Color::rgba(1.0, 1.0, 1.0, 0.0)); - let border_color = style.border_color.resolve_or(Color::TRANSPARENT); + let border_color = style + .border_color + .resolve_or(Color::rgba(1.0, 1.0, 1.0, 0.0)); let font = style .font @@ -88,7 +91,7 @@ impl From<&Style> for RenderPrimitive { border: style.border.resolve(), layout: Rect::default(), }, - RenderCommand::Text { content } => Self::Text { + RenderCommand::Text { content, alignment } => Self::Text { color: style.color.resolve(), content, font, @@ -97,6 +100,7 @@ impl From<&Style> for RenderPrimitive { properties: TextProperties { font_size, line_height, + alignment, ..Default::default() }, }, diff --git a/kayak_core/src/styles/corner.rs b/src/styles/corner.rs similarity index 96% rename from kayak_core/src/styles/corner.rs rename to src/styles/corner.rs index ca6ed93..15f66f3 100644 --- a/kayak_core/src/styles/corner.rs +++ b/src/styles/corner.rs @@ -1,234 +1,234 @@ -use std::ops::{Mul, MulAssign}; - -/// A struct for defining properties related to the corners of widgets -/// -/// This is useful for things like border radii, etc. -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct Corner<T> -where - T: Copy + Default + PartialEq, -{ - /// The value of the top-left corner - pub top_left: T, - /// The value of the top-right corner - pub top_right: T, - /// The value of the bottom-left corner - pub bottom_left: T, - /// The value of the bottom-right corner - pub bottom_right: T, -} - -impl<T> Corner<T> -where - T: Copy + Default + PartialEq, -{ - /// Creates a new `Corner` with values individually specified for each corner - /// - /// # Arguments - /// - /// * `top_left`: The top-left corner value - /// * `top_right`: The top_-right corner value - /// * `bottom_left`: The bottom_-left corner value - /// * `bottom_right`: The bottom_-right corner value - /// - pub fn new(top_left: T, top_right: T, bottom_left: T, bottom_right: T) -> Self { - Self { - top_left, - top_right, - bottom_left, - bottom_right, - } - } - - /// Creates a new `Corner` with matching top corners and matching bottom corners - /// - /// # Arguments - /// - /// * `top`: The value of the top corners - /// * `bottom`: The value of the bottom corners - /// - /// ``` - /// # use kayak_core::styles::Corner; - /// // Creates a `Corner` with only the top corners rounded - /// let corner_radius = Corner::vertical(10.0, 0.0); - /// - /// // Creates a `Corner` with only the bottom corners rounded - /// let corner_radius = Corner::vertical(0.0, 10.0); - /// ``` - pub fn vertical(top: T, bottom: T) -> Self { - Self { - top_left: top, - top_right: top, - bottom_left: bottom, - bottom_right: bottom, - } - } - - /// Creates a new `Corner` with matching left corners and matching right corners - /// - /// # Arguments - /// - /// * `left`: The value of the left corners - /// * `right`: The value of the right corners - /// - /// ``` - /// # use kayak_core::styles::Corner; - /// // Creates a `Corner` with only the left corners rounded - /// let corner_radius = Corner::horizontal(10.0, 0.0); - /// - /// // Creates a `Corner` with only the right corners rounded - /// let corner_radius = Corner::horizontal(0.0, 10.0); - /// ``` - pub fn horizontal(left: T, right: T) -> Self { - Self { - top_left: left, - top_right: right, - bottom_left: left, - bottom_right: right, - } - } - - /// Creates a new `Corner` with all corners having the same value - /// - /// # Arguments - /// - /// * `value`: The value of all corners - /// - pub fn all(value: T) -> Self { - Self { - top_left: value, - top_right: value, - bottom_left: value, - bottom_right: value, - } - } - - /// Converts this `Corner` into a tuple matching `(Top Left, Top Right, Bottom Left, Bottom Right)` - pub fn into_tuple(self) -> (T, T, T, T) { - ( - self.top_left, - self.top_right, - self.bottom_left, - self.bottom_right, - ) - } -} - -impl<T> From<Corner<T>> for (T, T, T, T) -where - T: Copy + Default + PartialEq, -{ - /// Creates a tuple matching the pattern: `(Top Left, Top Right, Bottom Left, Bottom Right)` - fn from(edge: Corner<T>) -> Self { - edge.into_tuple() - } -} - -impl<T> From<T> for Corner<T> -where - T: Copy + Default + PartialEq, -{ - fn from(value: T) -> Self { - Corner::all(value) - } -} - -impl<T> From<(T, T, T, T)> for Corner<T> -where - T: Copy + Default + PartialEq, -{ - /// Converts the tuple according to the pattern: `(Top Left, Top Right, Bottom Left, Bottom Right)` - fn from(value: (T, T, T, T)) -> Self { - Corner::new(value.0, value.1, value.2, value.3) - } -} - -impl<T> Mul<T> for Corner<T> -where - T: Copy + Default + PartialEq + Mul<Output = T>, -{ - type Output = Self; - - fn mul(self, rhs: T) -> Self::Output { - Self { - top_left: self.top_left * rhs, - top_right: self.top_right * rhs, - bottom_left: self.bottom_left * rhs, - bottom_right: self.bottom_right * rhs, - } - } -} - -impl<T> Mul<Corner<T>> for Corner<T> -where - T: Copy + Default + PartialEq + Mul<Output = T>, -{ - type Output = Self; - - fn mul(self, rhs: Corner<T>) -> Self::Output { - Self { - top_left: rhs.top_left * self.top_left, - top_right: rhs.top_right * self.top_right, - bottom_left: rhs.bottom_left * self.bottom_left, - bottom_right: rhs.bottom_right * self.bottom_right, - } - } -} - -impl<T> MulAssign<T> for Corner<T> -where - T: Copy + Default + PartialEq + MulAssign, -{ - fn mul_assign(&mut self, rhs: T) { - self.top_left *= rhs; - self.top_right *= rhs; - self.bottom_left *= rhs; - self.bottom_right *= rhs; - } -} - -impl<T> MulAssign<Corner<T>> for Corner<T> -where - T: Copy + Default + PartialEq + MulAssign, -{ - fn mul_assign(&mut self, rhs: Corner<T>) { - self.top_left *= rhs.top_left; - self.top_right *= rhs.top_right; - self.bottom_left *= rhs.bottom_left; - self.bottom_right *= rhs.bottom_right; - } -} - -#[cfg(test)] -mod tests { - use super::Corner; - - #[test] - fn tuples_should_convert_to_corner() { - let expected = (1.0, 2.0, 3.0, 4.0); - let corner: Corner<f32> = expected.into(); - assert_eq!(expected, corner.into_tuple()); - - let expected = (1.0, 1.0, 1.0, 1.0); - let corner: Corner<f32> = (expected.0).into(); - assert_eq!(expected, corner.into_tuple()); - - let expected = (1.0, 1.0, 1.0, 1.0); - let corner: Corner<f32> = expected.0.into(); - assert_eq!(expected, corner.into_tuple()); - } - - #[test] - fn multiplication_should_work_on_corners() { - let expected = (10.0, 20.0, 30.0, 40.0); - let mut corner = Corner::new(1.0, 2.0, 3.0, 4.0); - - // Basic multiplication - let multiplied = corner * 10.0; - assert_eq!(expected, multiplied.into_tuple()); - - // Multiply and assign - corner *= 10.0; - assert_eq!(expected, corner.into_tuple()); - } -} +use std::ops::{Mul, MulAssign}; + +/// A struct for defining properties related to the corners of widgets +/// +/// This is useful for things like border radii, etc. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct Corner<T> +where + T: Copy + Default + PartialEq, +{ + /// The value of the top-left corner + pub top_left: T, + /// The value of the top-right corner + pub top_right: T, + /// The value of the bottom-left corner + pub bottom_left: T, + /// The value of the bottom-right corner + pub bottom_right: T, +} + +impl<T> Corner<T> +where + T: Copy + Default + PartialEq, +{ + /// Creates a new `Corner` with values individually specified for each corner + /// + /// # Arguments + /// + /// * `top_left`: The top-left corner value + /// * `top_right`: The top_-right corner value + /// * `bottom_left`: The bottom_-left corner value + /// * `bottom_right`: The bottom_-right corner value + /// + pub fn new(top_left: T, top_right: T, bottom_left: T, bottom_right: T) -> Self { + Self { + top_left, + top_right, + bottom_left, + bottom_right, + } + } + + /// Creates a new `Corner` with matching top corners and matching bottom corners + /// + /// # Arguments + /// + /// * `top`: The value of the top corners + /// * `bottom`: The value of the bottom corners + /// + /// ``` + /// # use kayak_core::styles::Corner; + /// // Creates a `Corner` with only the top corners rounded + /// let corner_radius = Corner::vertical(10.0, 0.0); + /// + /// // Creates a `Corner` with only the bottom corners rounded + /// let corner_radius = Corner::vertical(0.0, 10.0); + /// ``` + pub fn vertical(top: T, bottom: T) -> Self { + Self { + top_left: top, + top_right: top, + bottom_left: bottom, + bottom_right: bottom, + } + } + + /// Creates a new `Corner` with matching left corners and matching right corners + /// + /// # Arguments + /// + /// * `left`: The value of the left corners + /// * `right`: The value of the right corners + /// + /// ``` + /// # use kayak_core::styles::Corner; + /// // Creates a `Corner` with only the left corners rounded + /// let corner_radius = Corner::horizontal(10.0, 0.0); + /// + /// // Creates a `Corner` with only the right corners rounded + /// let corner_radius = Corner::horizontal(0.0, 10.0); + /// ``` + pub fn horizontal(left: T, right: T) -> Self { + Self { + top_left: left, + top_right: right, + bottom_left: left, + bottom_right: right, + } + } + + /// Creates a new `Corner` with all corners having the same value + /// + /// # Arguments + /// + /// * `value`: The value of all corners + /// + pub fn all(value: T) -> Self { + Self { + top_left: value, + top_right: value, + bottom_left: value, + bottom_right: value, + } + } + + /// Converts this `Corner` into a tuple matching `(Top Left, Top Right, Bottom Left, Bottom Right)` + pub fn into_tuple(self) -> (T, T, T, T) { + ( + self.top_left, + self.top_right, + self.bottom_left, + self.bottom_right, + ) + } +} + +impl<T> From<Corner<T>> for (T, T, T, T) +where + T: Copy + Default + PartialEq, +{ + /// Creates a tuple matching the pattern: `(Top Left, Top Right, Bottom Left, Bottom Right)` + fn from(edge: Corner<T>) -> Self { + edge.into_tuple() + } +} + +impl<T> From<T> for Corner<T> +where + T: Copy + Default + PartialEq, +{ + fn from(value: T) -> Self { + Corner::all(value) + } +} + +impl<T> From<(T, T, T, T)> for Corner<T> +where + T: Copy + Default + PartialEq, +{ + /// Converts the tuple according to the pattern: `(Top Left, Top Right, Bottom Left, Bottom Right)` + fn from(value: (T, T, T, T)) -> Self { + Corner::new(value.0, value.1, value.2, value.3) + } +} + +impl<T> Mul<T> for Corner<T> +where + T: Copy + Default + PartialEq + Mul<Output = T>, +{ + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + Self { + top_left: self.top_left * rhs, + top_right: self.top_right * rhs, + bottom_left: self.bottom_left * rhs, + bottom_right: self.bottom_right * rhs, + } + } +} + +impl<T> Mul<Corner<T>> for Corner<T> +where + T: Copy + Default + PartialEq + Mul<Output = T>, +{ + type Output = Self; + + fn mul(self, rhs: Corner<T>) -> Self::Output { + Self { + top_left: rhs.top_left * self.top_left, + top_right: rhs.top_right * self.top_right, + bottom_left: rhs.bottom_left * self.bottom_left, + bottom_right: rhs.bottom_right * self.bottom_right, + } + } +} + +impl<T> MulAssign<T> for Corner<T> +where + T: Copy + Default + PartialEq + MulAssign, +{ + fn mul_assign(&mut self, rhs: T) { + self.top_left *= rhs; + self.top_right *= rhs; + self.bottom_left *= rhs; + self.bottom_right *= rhs; + } +} + +impl<T> MulAssign<Corner<T>> for Corner<T> +where + T: Copy + Default + PartialEq + MulAssign, +{ + fn mul_assign(&mut self, rhs: Corner<T>) { + self.top_left *= rhs.top_left; + self.top_right *= rhs.top_right; + self.bottom_left *= rhs.bottom_left; + self.bottom_right *= rhs.bottom_right; + } +} + +#[cfg(test)] +mod tests { + use super::Corner; + + #[test] + fn tuples_should_convert_to_corner() { + let expected = (1.0, 2.0, 3.0, 4.0); + let corner: Corner<f32> = expected.into(); + assert_eq!(expected, corner.into_tuple()); + + let expected = (1.0, 1.0, 1.0, 1.0); + let corner: Corner<f32> = (expected.0).into(); + assert_eq!(expected, corner.into_tuple()); + + let expected = (1.0, 1.0, 1.0, 1.0); + let corner: Corner<f32> = expected.0.into(); + assert_eq!(expected, corner.into_tuple()); + } + + #[test] + fn multiplication_should_work_on_corners() { + let expected = (10.0, 20.0, 30.0, 40.0); + let mut corner = Corner::new(1.0, 2.0, 3.0, 4.0); + + // Basic multiplication + let multiplied = corner * 10.0; + assert_eq!(expected, multiplied.into_tuple()); + + // Multiply and assign + corner *= 10.0; + assert_eq!(expected, corner.into_tuple()); + } +} diff --git a/kayak_core/src/styles/edge.rs b/src/styles/edge.rs similarity index 95% rename from kayak_core/src/styles/edge.rs rename to src/styles/edge.rs index a62d669..84f1f0c 100644 --- a/kayak_core/src/styles/edge.rs +++ b/src/styles/edge.rs @@ -1,208 +1,208 @@ -use std::ops::{Mul, MulAssign}; - -/// A struct for defining properties related to the edges of widgets -/// -/// This is useful for things like borders, padding, etc. -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct Edge<T> -where - T: Copy + Default + PartialEq, -{ - /// The value of the top edge - pub top: T, - /// The value of the right edge - pub right: T, - /// The value of the bottom edge - pub bottom: T, - /// The value of the left edge - pub left: T, -} - -impl<T> Edge<T> -where - T: Copy + Default + PartialEq, -{ - /// Creates a new `Edge` with values individually specified for each edge - /// - /// # Arguments - /// - /// * `top`: The top edge value - /// * `right`: The right edge value - /// * `bottom`: The bottom edge value - /// * `left`: The left edge value - /// - pub fn new(top: T, right: T, bottom: T, left: T) -> Self { - Self { - top, - right, - bottom, - left, - } - } - - /// Creates a new `Edge` with matching vertical edges and matching horizontal edges - /// - /// # Arguments - /// - /// * `vertical`: The value of the vertical edges - /// * `horizontal`: The value of the horizontal edges - /// - pub fn axis(vertical: T, horizontal: T) -> Self { - Self { - top: vertical, - right: horizontal, - bottom: vertical, - left: horizontal, - } - } - - /// Creates a new `Edge` with all edges having the same value - /// - /// # Arguments - /// - /// * `value`: The value of all edges - /// - pub fn all(value: T) -> Self { - Self { - top: value, - right: value, - bottom: value, - left: value, - } - } - - /// Converts this `Edge` into a tuple matching `(Top, Right, Bottom, Left)` - pub fn into_tuple(self) -> (T, T, T, T) { - (self.top, self.right, self.bottom, self.left) - } -} - -impl<T> From<Edge<T>> for (T, T, T, T) -where - T: Copy + Default + PartialEq, -{ - fn from(edge: Edge<T>) -> Self { - edge.into_tuple() - } -} - -impl<T> From<T> for Edge<T> -where - T: Copy + Default + PartialEq, -{ - fn from(value: T) -> Self { - Edge::all(value) - } -} - -impl<T> From<(T, T)> for Edge<T> -where - T: Copy + Default + PartialEq, -{ - fn from(value: (T, T)) -> Self { - Edge::axis(value.0, value.1) - } -} - -impl<T> From<(T, T, T, T)> for Edge<T> -where - T: Copy + Default + PartialEq, -{ - fn from(value: (T, T, T, T)) -> Self { - Edge::new(value.0, value.1, value.2, value.3) - } -} - -impl<T> Mul<T> for Edge<T> -where - T: Copy + Default + PartialEq + Mul<Output = T>, -{ - type Output = Self; - - fn mul(self, rhs: T) -> Self::Output { - Self { - top: self.top * rhs, - right: self.right * rhs, - bottom: self.bottom * rhs, - left: self.left * rhs, - } - } -} - -impl<T> Mul<Edge<T>> for Edge<T> -where - T: Copy + Default + PartialEq + Mul<Output = T>, -{ - type Output = Self; - - fn mul(self, rhs: Edge<T>) -> Self::Output { - Self { - top: rhs.top * self.top, - right: rhs.right * self.right, - bottom: rhs.bottom * self.bottom, - left: rhs.left * self.left, - } - } -} - -impl<T> MulAssign<T> for Edge<T> -where - T: Copy + Default + PartialEq + MulAssign, -{ - fn mul_assign(&mut self, rhs: T) { - self.top *= rhs; - self.right *= rhs; - self.bottom *= rhs; - self.left *= rhs; - } -} - -impl<T> MulAssign<Edge<T>> for Edge<T> -where - T: Copy + Default + PartialEq + MulAssign, -{ - fn mul_assign(&mut self, rhs: Edge<T>) { - self.top *= rhs.top; - self.right *= rhs.right; - self.bottom *= rhs.bottom; - self.left *= rhs.left; - } -} - -#[cfg(test)] -mod tests { - use super::Edge; - - #[test] - fn tuples_should_convert_to_edge() { - let expected = (1.0, 2.0, 3.0, 4.0); - let edge: Edge<f32> = expected.into(); - assert_eq!(expected, edge.into_tuple()); - - let expected = (1.0, 2.0, 1.0, 2.0); - let edge: Edge<f32> = (expected.0, expected.1).into(); - assert_eq!(expected, edge.into_tuple()); - - let expected = (1.0, 1.0, 1.0, 1.0); - let edge: Edge<f32> = (expected.0).into(); - assert_eq!(expected, edge.into_tuple()); - - let expected = (1.0, 1.0, 1.0, 1.0); - let edge: Edge<f32> = expected.0.into(); - assert_eq!(expected, edge.into_tuple()); - } - - #[test] - fn multiplication_should_work_on_edges() { - let expected = (10.0, 20.0, 30.0, 40.0); - let mut corner = Edge::new(1.0, 2.0, 3.0, 4.0); - - // Basic multiplication - let multiplied = corner * 10.0; - assert_eq!(expected, multiplied.into_tuple()); - - // Multiply and assign - corner *= 10.0; - assert_eq!(expected, corner.into_tuple()); - } -} +use std::ops::{Mul, MulAssign}; + +/// A struct for defining properties related to the edges of widgets +/// +/// This is useful for things like borders, padding, etc. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct Edge<T> +where + T: Copy + Default + PartialEq, +{ + /// The value of the top edge + pub top: T, + /// The value of the right edge + pub right: T, + /// The value of the bottom edge + pub bottom: T, + /// The value of the left edge + pub left: T, +} + +impl<T> Edge<T> +where + T: Copy + Default + PartialEq, +{ + /// Creates a new `Edge` with values individually specified for each edge + /// + /// # Arguments + /// + /// * `top`: The top edge value + /// * `right`: The right edge value + /// * `bottom`: The bottom edge value + /// * `left`: The left edge value + /// + pub fn new(top: T, right: T, bottom: T, left: T) -> Self { + Self { + top, + right, + bottom, + left, + } + } + + /// Creates a new `Edge` with matching vertical edges and matching horizontal edges + /// + /// # Arguments + /// + /// * `vertical`: The value of the vertical edges + /// * `horizontal`: The value of the horizontal edges + /// + pub fn axis(vertical: T, horizontal: T) -> Self { + Self { + top: vertical, + right: horizontal, + bottom: vertical, + left: horizontal, + } + } + + /// Creates a new `Edge` with all edges having the same value + /// + /// # Arguments + /// + /// * `value`: The value of all edges + /// + pub fn all(value: T) -> Self { + Self { + top: value, + right: value, + bottom: value, + left: value, + } + } + + /// Converts this `Edge` into a tuple matching `(Top, Right, Bottom, Left)` + pub fn into_tuple(self) -> (T, T, T, T) { + (self.top, self.right, self.bottom, self.left) + } +} + +impl<T> From<Edge<T>> for (T, T, T, T) +where + T: Copy + Default + PartialEq, +{ + fn from(edge: Edge<T>) -> Self { + edge.into_tuple() + } +} + +impl<T> From<T> for Edge<T> +where + T: Copy + Default + PartialEq, +{ + fn from(value: T) -> Self { + Edge::all(value) + } +} + +impl<T> From<(T, T)> for Edge<T> +where + T: Copy + Default + PartialEq, +{ + fn from(value: (T, T)) -> Self { + Edge::axis(value.0, value.1) + } +} + +impl<T> From<(T, T, T, T)> for Edge<T> +where + T: Copy + Default + PartialEq, +{ + fn from(value: (T, T, T, T)) -> Self { + Edge::new(value.0, value.1, value.2, value.3) + } +} + +impl<T> Mul<T> for Edge<T> +where + T: Copy + Default + PartialEq + Mul<Output = T>, +{ + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + Self { + top: self.top * rhs, + right: self.right * rhs, + bottom: self.bottom * rhs, + left: self.left * rhs, + } + } +} + +impl<T> Mul<Edge<T>> for Edge<T> +where + T: Copy + Default + PartialEq + Mul<Output = T>, +{ + type Output = Self; + + fn mul(self, rhs: Edge<T>) -> Self::Output { + Self { + top: rhs.top * self.top, + right: rhs.right * self.right, + bottom: rhs.bottom * self.bottom, + left: rhs.left * self.left, + } + } +} + +impl<T> MulAssign<T> for Edge<T> +where + T: Copy + Default + PartialEq + MulAssign, +{ + fn mul_assign(&mut self, rhs: T) { + self.top *= rhs; + self.right *= rhs; + self.bottom *= rhs; + self.left *= rhs; + } +} + +impl<T> MulAssign<Edge<T>> for Edge<T> +where + T: Copy + Default + PartialEq + MulAssign, +{ + fn mul_assign(&mut self, rhs: Edge<T>) { + self.top *= rhs.top; + self.right *= rhs.right; + self.bottom *= rhs.bottom; + self.left *= rhs.left; + } +} + +#[cfg(test)] +mod tests { + use super::Edge; + + #[test] + fn tuples_should_convert_to_edge() { + let expected = (1.0, 2.0, 3.0, 4.0); + let edge: Edge<f32> = expected.into(); + assert_eq!(expected, edge.into_tuple()); + + let expected = (1.0, 2.0, 1.0, 2.0); + let edge: Edge<f32> = (expected.0, expected.1).into(); + assert_eq!(expected, edge.into_tuple()); + + let expected = (1.0, 1.0, 1.0, 1.0); + let edge: Edge<f32> = (expected.0).into(); + assert_eq!(expected, edge.into_tuple()); + + let expected = (1.0, 1.0, 1.0, 1.0); + let edge: Edge<f32> = expected.0.into(); + assert_eq!(expected, edge.into_tuple()); + } + + #[test] + fn multiplication_should_work_on_edges() { + let expected = (10.0, 20.0, 30.0, 40.0); + let mut corner = Edge::new(1.0, 2.0, 3.0, 4.0); + + // Basic multiplication + let multiplied = corner * 10.0; + assert_eq!(expected, multiplied.into_tuple()); + + // Multiply and assign + corner *= 10.0; + assert_eq!(expected, corner.into_tuple()); + } +} diff --git a/src/styles/mod.rs b/src/styles/mod.rs new file mode 100644 index 0000000..c6c91ac --- /dev/null +++ b/src/styles/mod.rs @@ -0,0 +1,11 @@ +mod corner; +mod edge; +mod options_ref; +mod render_command; +mod style; + +pub use corner::Corner; +pub use edge::Edge; +pub use options_ref::AsRefOption; +pub use render_command::RenderCommand; +pub use style::*; diff --git a/src/styles/options_ref.rs b/src/styles/options_ref.rs new file mode 100644 index 0000000..2e2dc17 --- /dev/null +++ b/src/styles/options_ref.rs @@ -0,0 +1,30 @@ +use crate::styles::KStyle; + +/// A trait used to allow reading a value as an `Option<&T>` +pub trait AsRefOption<T> { + fn as_ref_option(&self) -> Option<&T>; +} + +impl AsRefOption<KStyle> for KStyle { + fn as_ref_option(&self) -> Option<&KStyle> { + Some(&self) + } +} + +impl AsRefOption<KStyle> for &KStyle { + fn as_ref_option(&self) -> Option<&KStyle> { + Some(self) + } +} + +impl AsRefOption<KStyle> for Option<KStyle> { + fn as_ref_option(&self) -> Option<&KStyle> { + self.as_ref() + } +} + +impl AsRefOption<KStyle> for &Option<KStyle> { + fn as_ref_option(&self) -> Option<&KStyle> { + self.as_ref() + } +} diff --git a/kayak_core/src/render_command.rs b/src/styles/render_command.rs similarity index 59% rename from kayak_core/src/render_command.rs rename to src/styles/render_command.rs index 13f9cba..d2f242c 100644 --- a/kayak_core/src/render_command.rs +++ b/src/styles/render_command.rs @@ -1,31 +1,35 @@ -use crate::styles::Edge; - -#[derive(Debug, Clone, PartialEq)] -pub enum RenderCommand { - Empty, - /// Represents a node that has no renderable object but contributes to the layout. - Layout, - Clip, - Quad, - Text { - content: String, - }, - Image { - handle: u16, - }, - TextureAtlas { - position: (f32, f32), - size: (f32, f32), - handle: u16, - }, - NinePatch { - border: Edge<f32>, - handle: u16, - }, -} - -impl Default for RenderCommand { - fn default() -> Self { - Self::Empty - } -} +use bevy::prelude::{Handle, Image, Vec2}; +use kayak_font::Alignment; + +use super::Edge; + +#[derive(Debug, Clone, PartialEq)] +pub enum RenderCommand { + Empty, + /// Represents a node that has no renderable object but contributes to the layout. + Layout, + Clip, + Quad, + Text { + content: String, + alignment: Alignment, + }, + Image { + handle: Handle<Image>, + }, + TextureAtlas { + position: Vec2, + size: Vec2, + handle: Handle<Image>, + }, + NinePatch { + border: Edge<f32>, + handle: Handle<Image>, + }, +} + +impl Default for RenderCommand { + fn default() -> Self { + Self::Empty + } +} diff --git a/kayak_core/src/styles/mod.rs b/src/styles/style.rs similarity index 92% rename from kayak_core/src/styles/mod.rs rename to src/styles/style.rs index 1aecd64..a28d0c3 100644 --- a/kayak_core/src/styles/mod.rs +++ b/src/styles/style.rs @@ -1,18 +1,28 @@ //! Contains code related to the styling of widgets -mod corner; -mod edge; -mod option_ref; - use std::ops::Add; -pub use corner::Corner; -pub use edge::Edge; +use bevy::prelude::Color; +use bevy::prelude::Component; +use bevy::window::CursorIcon; pub use morphorm::{LayoutType, PositionType, Units}; use crate::cursor::PointerEvents; -use crate::{color::Color, render_command::RenderCommand, CursorIcon}; -use option_ref::AsRefOption; + +use super::AsRefOption; +pub use super::Corner; +pub use super::Edge; +use super::RenderCommand; + +/// Just a wrapper around bevy's CursorIcon so we can define a default. +#[derive(Debug, Clone, PartialEq)] +pub struct KCursorIcon(pub CursorIcon); + +impl Default for KCursorIcon { + fn default() -> Self { + Self(CursorIcon::Default) + } +} /// The base container of all style properties /// @@ -171,7 +181,7 @@ macro_rules! define_styles { /// Applies a `Style` over this one /// /// Values from `other` are applied to any field in this one that is marked as [`StyleProp::Unset`] - pub fn apply<T: AsRefOption<Style>>(&mut self, other: T) { + pub fn apply<T: AsRefOption<KStyle>>(&mut self, other: T) { if let Some(other) = other.as_ref_option() { $( if matches!(self.$field, StyleProp::Unset) { @@ -184,7 +194,7 @@ macro_rules! define_styles { /// Applies the given style and returns the updated style /// /// This is simply a builder-like wrapper around the [`Style::apply`] method. - pub fn with_style<T: AsRefOption<Style>>(mut self, other: T) -> Self { + pub fn with_style<T: AsRefOption<KStyle>>(mut self, other: T) -> Self { self.apply(other); self } @@ -229,8 +239,8 @@ define_styles! { /// // Applied second (sets any remaining `StyleProp::Unset` fields) /// .with_style(&style_b); /// ``` - #[derive(Debug, Default, Clone, PartialEq)] - pub struct Style { + #[derive(Component, Debug, Default, Clone, PartialEq)] + pub struct KStyle { /// The background color of this widget /// /// Only applies to widgets marked [`RenderCommand::Quad`] @@ -265,7 +275,7 @@ define_styles! { /// The spacing between child widgets along the horizontal axis pub col_between: StyleProp<Units>, /// The cursor icon to display when hovering this widget - pub cursor: StyleProp<CursorIcon>, + pub cursor: StyleProp<KCursorIcon>, /// The font name for this widget /// /// Only applies to [`RenderCommand::Text`] @@ -354,7 +364,7 @@ define_styles! { } } -impl Style { +impl KStyle { /// Returns a `Style` object where all fields are set to their own initial values /// /// This is the actual "default" to apply over any field marked as [`StyleProp::Unset`] before @@ -396,26 +406,26 @@ impl Style { } } -impl Add for Style { - type Output = Style; +impl Add for KStyle { + type Output = KStyle; /// Defines the `+` operator for [`Style`]. This is a convenience wrapper of the `self.with_style()` method and useful for concatenating many small `Style` variables. /// Similar to `with_style()` In a `StyleA + StyleB` operation, values from `StyleB` are applied to any field of StyleA that are marked as [`StyleProp::Unset`]. /// /// Note: since the changes are applied only to unset fields, addition is *not* commutative. This means StyleA + StyleB != StyleB + StyleA for most cases. - fn add(self, other: Style) -> Style { + fn add(self, other: KStyle) -> KStyle { self.with_style(other) } } #[cfg(test)] mod tests { - use super::{Edge, Style, StyleProp, Units}; + use super::{Edge, KStyle, StyleProp, Units}; #[test] fn styles_should_equal() { - let mut a = Style::default(); - let mut b = Style::default(); + let mut a = KStyle::default(); + let mut b = KStyle::default(); assert_eq!(a, b); a.height = StyleProp::Default; @@ -427,11 +437,11 @@ mod tests { fn style_should_inherit_property() { let border = Edge::new(1.0, 2.0, 3.0, 4.0); - let parent = Style { + let parent = KStyle { border: StyleProp::Value(border), ..Default::default() }; - let mut child = Style { + let mut child = KStyle { border: StyleProp::Inherit, ..Default::default() }; @@ -444,7 +454,7 @@ mod tests { #[test] #[should_panic] fn style_should_panic_on_resolve_inherit_property() { - let style = Style { + let style = KStyle { color: StyleProp::Inherit, ..Default::default() }; @@ -454,8 +464,8 @@ mod tests { #[test] fn style_should_apply_styles_on_unset_property() { - let mut base_style = Style::default(); - let other_style = Style { + let mut base_style = KStyle::default(); + let other_style = KStyle { width: StyleProp::Value(Units::Pixels(123.0)), ..Default::default() }; @@ -469,11 +479,11 @@ mod tests { #[test] fn style_should_not_apply_styles_on_non_unset_property() { - let mut base_style = Style { + let mut base_style = KStyle { width: StyleProp::Default, ..Default::default() }; - let other_style = Style { + let other_style = KStyle { width: StyleProp::Value(Units::Pixels(123.0)), ..Default::default() }; @@ -487,8 +497,8 @@ mod tests { #[test] fn style_should_apply_option_style() { - let mut base_style = Style::default(); - let other_style = Some(Style { + let mut base_style = KStyle::default(); + let other_style = Some(KStyle { width: StyleProp::Value(Units::Pixels(123.0)), ..Default::default() }); @@ -506,7 +516,7 @@ mod tests { #[test] fn style_should_not_apply_none() { - let expected = Style::default(); + let expected = KStyle::default(); let mut base_style = expected.clone(); assert_eq!(expected, base_style); @@ -520,26 +530,26 @@ mod tests { let expected_width = StyleProp::Value(Units::Stretch(1.0)); let expected_height = StyleProp::Inherit; - let expected = Style { + let expected = KStyle { left: expected_left.clone(), width: expected_width.clone(), height: expected_height.clone(), ..Default::default() }; - let style = Style::default() + let style = KStyle::default() // Pass ownership - .with_style(Style { + .with_style(KStyle { height: expected_height, ..Default::default() }) // Pass ownership of option - .with_style(Some(Style { + .with_style(Some(KStyle { left: expected_left, ..Default::default() })) // Pass reference - .with_style(&Style { + .with_style(&KStyle { width: expected_width, ..Default::default() }); @@ -553,23 +563,23 @@ mod tests { let expected_width = StyleProp::Value(Units::Stretch(1.0)); let expected_height = StyleProp::Inherit; - let expected = Style { + let expected = KStyle { left: expected_left.clone(), width: expected_width.clone(), height: expected_height.clone(), ..Default::default() }; - let style_a = Style::default(); - let style_b = Style { + let style_a = KStyle::default(); + let style_b = KStyle { height: expected_height, ..Default::default() }; - let style_c = Style { + let style_c = KStyle { left: expected_left, ..Default::default() }; - let style_d = Style { + let style_d = KStyle { width: expected_width, ..Default::default() }; diff --git a/kayak_core/src/tree.rs b/src/tree.rs similarity index 77% rename from kayak_core/src/tree.rs rename to src/tree.rs index daa68c2..1f64537 100644 --- a/kayak_core/src/tree.rs +++ b/src/tree.rs @@ -1,18 +1,15 @@ -use std::iter::Rev; -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; - +use bevy::prelude::Entity; +use bevy::utils::HashMap; use morphorm::Hierarchy; +use std::iter::Rev; -use crate::{Arena, BoxedWidget, Index}; +use crate::node::WrappedIndex; -#[derive(Default, Debug, PartialEq)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct Tree { - pub children: HashMap<Index, Vec<Index>>, - pub parents: HashMap<Index, Index>, - pub root_node: Option<Index>, + pub children: HashMap<WrappedIndex, Vec<WrappedIndex>>, + pub parents: HashMap<WrappedIndex, WrappedIndex>, + pub root_node: Option<WrappedIndex>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -26,7 +23,7 @@ pub enum Change { #[derive(Default, Debug, Clone)] pub struct ChildChanges { - pub changes: Vec<(usize, Index, Index, Vec<Change>)>, + pub changes: Vec<(usize, WrappedIndex, WrappedIndex, Vec<Change>)>, pub child_changes: Vec<(usize, ChildChanges)>, } @@ -39,8 +36,8 @@ impl ChildChanges { } } -impl From<Vec<(usize, Index, Index, Vec<Change>)>> for ChildChanges { - fn from(changes: Vec<(usize, Index, Index, Vec<Change>)>) -> Self { +impl From<Vec<(usize, WrappedIndex, WrappedIndex, Vec<Change>)>> for ChildChanges { + fn from(changes: Vec<(usize, WrappedIndex, WrappedIndex, Vec<Change>)>) -> Self { Self { changes, child_changes: Vec::new(), @@ -49,7 +46,7 @@ impl From<Vec<(usize, Index, Index, Vec<Change>)>> for ChildChanges { } impl Tree { - pub fn add(&mut self, index: Index, parent: Option<Index>) { + pub fn add(&mut self, index: WrappedIndex, parent: Option<WrappedIndex>) { if let Some(parent_index) = parent { self.parents.insert(index, parent_index); if let Some(parent_children) = self.children.get_mut(&parent_index) { @@ -63,7 +60,7 @@ impl Tree { } /// Remove the given node and recursively removes its descendants - pub fn remove(&mut self, index: Index) -> Vec<Index> { + pub fn remove(&mut self, index: WrappedIndex) -> Vec<WrappedIndex> { let parent = self.parents.remove(&index); if let Some(parent) = parent { let children = self @@ -89,12 +86,18 @@ impl Tree { } } + pub fn remove_children(&mut self, children_to_remove: Vec<WrappedIndex>) { + for child in children_to_remove.iter() { + self.remove(*child); + } + } + /// Removes the current node and reparents any children to its current parent. /// /// Children fill at the original index of the removed node amongst its siblings. /// /// Panics if called on the root node - pub fn remove_and_reparent(&mut self, index: Index) { + pub fn remove_and_reparent(&mut self, index: WrappedIndex) { let parent = self.parents.remove(&index); if let Some(parent) = parent { let mut insertion_index = 0usize; @@ -119,7 +122,7 @@ impl Tree { } /// Replace the given node with another, transferring the parent and child relationships over to the replacement node - pub fn replace(&mut self, index: Index, replace_with: Index) { + pub fn replace(&mut self, index: WrappedIndex, replace_with: WrappedIndex) { // === Update Parent === // if let Some(parent) = self.parents.remove(&index) { self.parents.insert(replace_with, parent); @@ -141,7 +144,7 @@ impl Tree { } /// Returns true if the given node is in this tree - pub fn contains(&self, index: Index) -> bool { + pub fn contains(&self, index: WrappedIndex) -> bool { Some(index) == self.root_node || self.parents.contains_key(&index) || self.children.contains_key(&index) @@ -162,7 +165,7 @@ impl Tree { } /// Returns true if the given node is a descendant of another node - pub fn is_descendant(&self, descendant: Index, of_node: Index) -> bool { + pub fn is_descendant(&self, descendant: WrappedIndex, of_node: WrappedIndex) -> bool { let mut index = descendant; while let Some(parent) = self.get_parent(index) { index = parent; @@ -173,7 +176,7 @@ impl Tree { false } - pub fn flatten(&self) -> Vec<Index> { + pub fn flatten(&self) -> Vec<WrappedIndex> { if self.root_node.is_none() { return Vec::new(); } @@ -181,7 +184,7 @@ impl Tree { DownwardIterator::new(&self, Some(self.root_node.unwrap()), true).collect::<Vec<_>>() } - pub fn flatten_node(&self, root_node: Index) -> Vec<Index> { + pub fn flatten_node(&self, root_node: WrappedIndex) -> Vec<WrappedIndex> { if self.root_node.is_none() { return Vec::new(); } @@ -189,13 +192,21 @@ impl Tree { DownwardIterator::new(&self, Some(root_node), true).collect::<Vec<_>>() } - pub fn get_parent(&self, index: Index) -> Option<Index> { + pub fn flatten_node_up(&self, root_node: WrappedIndex) -> Vec<WrappedIndex> { + if self.root_node.is_none() { + return Vec::new(); + } + + UpwardIterator::new(&self, Some(root_node), true).collect::<Vec<_>>() + } + + pub fn get_parent(&self, index: WrappedIndex) -> Option<WrappedIndex> { self.parents .get(&index) .map_or(None, |parent| Some(*parent)) } - pub fn get_first_child(&self, index: Index) -> Option<Index> { + pub fn get_first_child(&self, index: WrappedIndex) -> Option<WrappedIndex> { self.children.get(&index).map_or(None, |children| { children .first() @@ -203,13 +214,13 @@ impl Tree { }) } - pub fn get_last_child(&self, index: Index) -> Option<Index> { + pub fn get_last_child(&self, index: WrappedIndex) -> Option<WrappedIndex> { self.children.get(&index).map_or(None, |children| { children.last().map_or(None, |last_child| Some(*last_child)) }) } - pub fn get_next_sibling(&self, index: Index) -> Option<Index> { + pub fn get_next_sibling(&self, index: WrappedIndex) -> Option<WrappedIndex> { if let Some(parent_index) = self.get_parent(index) { self.children.get(&parent_index).map_or(None, |children| { children @@ -226,7 +237,7 @@ impl Tree { } } - pub fn get_prev_sibling(&self, index: Index) -> Option<Index> { + pub fn get_prev_sibling(&self, index: WrappedIndex) -> Option<WrappedIndex> { if let Some(parent_index) = self.get_parent(index) { self.children.get(&parent_index).map_or(None, |children| { children @@ -247,9 +258,15 @@ impl Tree { } } - pub fn diff_children(&self, other_tree: &Tree, root_node: Index) -> ChildChanges { + pub fn diff_children( + &self, + other_tree: &Tree, + root_node: WrappedIndex, + depth: u32, + ) -> ChildChanges { let children_a = self.children.get(&root_node); let children_b = other_tree.children.get(&root_node); + // Handle both easy cases first.. if children_a.is_some() && children_b.is_none() { return children_a @@ -282,13 +299,13 @@ impl Tree { .into_iter() .map(|i| *i) .enumerate() - .collect::<Vec<(usize, Index)>>(); + .collect::<Vec<(usize, WrappedIndex)>>(); let children_b = children_b .unwrap() .into_iter() .map(|i| *i) .enumerate() - .collect::<Vec<(usize, Index)>>(); + .collect::<Vec<(usize, WrappedIndex)>>(); let deleted_nodes = children_a .iter() @@ -302,8 +319,9 @@ impl Tree { .iter() .map(|(id, node)| { let old_node = children_a.get(*id); - let inserted = - old_node.is_some() && !children_a.iter().any(|(_, old_node)| node == old_node); + let inserted = old_node.is_none() + || old_node.is_some() + && !children_a.iter().any(|(_, old_node)| node == old_node); let value_changed = if let Some((_, old_node)) = old_node { node != old_node @@ -322,6 +340,19 @@ impl Tree { .collect::<Vec<_>>(); child_changes.changes.extend(inserted_and_changed); + if child_changes.changes.len() > 0 + && child_changes.changes.iter().any(|a| { + child_changes + .changes + .iter() + .any(|b| a.1 == b.1 && a.3 != b.3) + }) + { + dbg!("ABORT!"); + dbg!(&children_a); + dbg!(&children_b); + } + let flat_tree_diff_nodes = child_changes .changes .iter() @@ -370,13 +401,16 @@ impl Tree { .collect::<Vec<_>>(); child_changes.changes = flat_tree_diff_nodes; - // for (child_id, child_node) in children_a.iter() { - // // Add children of child changes. - // let children_of_child_changes = self.diff_children(other_tree, *child_node); - // child_changes - // .child_changes - // .push((*child_id, children_of_child_changes)); - // } + if depth > 0 { + for (child_id, child_node) in children_a.iter() { + // Add children of child changes. + let children_of_child_changes = + self.diff_children(other_tree, *child_node, depth - 1); + child_changes + .child_changes + .push((*child_id, children_of_child_changes)); + } + } child_changes } @@ -384,8 +418,8 @@ impl Tree { pub fn diff( &self, other_tree: &Tree, - root_node: Index, - ) -> Vec<(usize, Index, Index, Vec<Change>)> { + root_node: WrappedIndex, + ) -> Vec<(usize, WrappedIndex, WrappedIndex, Vec<Change>)> { let mut changes = Vec::new(); let mut tree1 = self @@ -494,9 +528,15 @@ impl Tree { flat_tree_diff_nodes } - pub fn merge(&mut self, other: &Tree, root_node: Index, changes: ChildChanges) { + pub fn merge( + &mut self, + other: &Tree, + root_node: WrappedIndex, + changes: ChildChanges, + depth: u32, + ) { let has_changes = changes.has_changes(); - let children_a = self.children.get_mut(&root_node); + let children_a = self.children.get(&root_node).cloned(); let children_b = other.children.get(&root_node); if children_a.is_none() && children_b.is_none() { // Nothing to do. @@ -504,6 +544,11 @@ impl Tree { } else if children_a.is_none() && children_b.is_some() { // Simple case of moving all children over to A. self.children.insert(root_node, children_b.unwrap().clone()); + for (parent, children) in self.children.iter() { + for child in children.iter() { + self.parents.insert(*child, *parent); + } + } return; } else if children_a.is_some() && children_b.is_none() { // Case for erasing all @@ -516,11 +561,19 @@ impl Tree { } return; } - let children_a = children_a.unwrap(); + let mut children_a = children_a.unwrap(); let children_b = children_b.unwrap(); - children_a.resize(children_b.len(), Index::default()); + children_a.resize(children_b.len(), WrappedIndex(Entity::from_raw(0))); + for (id, node, parent_node, change) in changes.changes.iter() { match change.as_slice() { + [Change::Deleted] => { + // self.parents.remove(node); + if children_a.get(*id).is_some() { + children_a[*id] = WrappedIndex(Entity::from_raw(0)); + } + self.remove(*node); + } [Change::Inserted] => { children_a[*id] = *node; self.parents.insert(*node, *parent_node); @@ -532,20 +585,51 @@ impl Tree { [Change::Updated] => { children_a[*id] = *node; } - [Change::Deleted] => { - self.parents.remove(node); - } _ => {} } } - // for (child_id, children_of_child_changes) in changes.child_changes { - // self.merge( - // other, - // changes.changes[child_id].1, - // children_of_child_changes, - // ); - // } + for (id, _node, _parent_node, _change) in changes.changes.iter() { + if let Some(child) = children_a.get(*id) { + if child.0.id() == 0 { + children_a.remove(*id); + } + } + } + + self.children.insert(root_node, children_a); + + if depth > 0 { + for (child_id, children_of_child_changes) in changes.child_changes { + self.merge( + other, + changes.changes[child_id].1, + children_of_child_changes, + depth - 1, + ); + } + } + } + + pub fn remove_child_from_node(&mut self, parent: &WrappedIndex, child: &WrappedIndex) { + if let Some(children) = self.children.get_mut(parent) { + let child_index = children.iter().position(|c| c == child); + + if let Some(child_index) = child_index { + children.remove(child_index); + } + } + } + + /// Copies a specific node and it's children from other_tree to self. + /// Note: Does not deep copy. + pub fn copy_from_point(&mut self, other_tree: &Tree, root_node: WrappedIndex) { + if let Some(children) = other_tree.children.get(&root_node) { + self.children.insert(root_node, children.clone()); + for child in children.iter() { + self.parents.insert(*child, root_node); + } + } } /// Dumps the tree's current state to the console @@ -557,9 +641,9 @@ impl Tree { /// * `widgets`: Optionally, provide the current widgets to include metadata about each widget /// /// returns: () - pub fn dump(&self, widgets: Option<&Arena<Option<BoxedWidget>>>) { + pub fn dump(&self) { if let Some(root) = self.root_node { - self.dump_at_internal(root, 0, widgets); + self.dump_at(root); } } @@ -573,38 +657,18 @@ impl Tree { /// * `widgets`: Optionally, provide the current widgets to include metadata about each widget /// /// returns: () - pub fn dump_at(&self, start_index: Index, widgets: Option<&Arena<Option<BoxedWidget>>>) { - self.dump_at_internal(start_index, 0, widgets); + pub fn dump_at(&self, start_index: WrappedIndex) { + self.dump_at_internal(start_index, 0); } - fn dump_at_internal( - &self, - start_index: Index, - depth: usize, - widgets: Option<&Arena<Option<BoxedWidget>>>, - ) { - let mut name = None; - if let Some(widgets) = widgets { - if let Some(widget) = widgets.get(start_index) { - if let Some(widget) = widget { - name = Some(widget.get_name()); - } - } - } - - let indent = "\t".repeat(depth); - let raw_parts = start_index.into_raw_parts(); - println!( - "{}{} [{}:{}]", - indent, - name.unwrap_or_default(), - raw_parts.0, - raw_parts.1 - ); + fn dump_at_internal(&self, start_index: WrappedIndex, depth: usize) { + let indent = " ".repeat(depth); + let raw_parts = start_index.0.id(); + println!("{} [{}]", indent, raw_parts,); if let Some(children) = self.children.get(&start_index) { for node_index in children { - self.dump_at_internal(*node_index, depth + 1, widgets); + self.dump_at_internal(*node_index, depth + 1); } } } @@ -613,10 +677,10 @@ impl Tree { /// An iterator that performs a depth-first traversal down a tree starting /// from a given node. pub struct DownwardIterator<'a> { - tree: &'a Tree, - starting_node: Option<Index>, - current_node: Option<Index>, - include_self: bool, + pub tree: &'a Tree, + pub starting_node: Option<WrappedIndex>, + pub current_node: Option<WrappedIndex>, + pub include_self: bool, } impl<'a> DownwardIterator<'a> { @@ -630,8 +694,8 @@ impl<'a> DownwardIterator<'a> { /// /// /// [tree]: Tree - /// [node]: Index - pub fn new(tree: &'a Tree, starting_node: Option<Index>, include_self: bool) -> Self { + /// [node]: WrappedIndex + pub fn new(tree: &'a Tree, starting_node: Option<WrappedIndex>, include_self: bool) -> Self { Self { tree, starting_node, @@ -642,7 +706,7 @@ impl<'a> DownwardIterator<'a> { } impl<'a> Iterator for DownwardIterator<'a> { - type Item = Index; + type Item = WrappedIndex; fn next(&mut self) -> Option<Self::Item> { if self.include_self { @@ -692,7 +756,7 @@ impl<'a> Iterator for DownwardIterator<'a> { /// from a given node. pub struct UpwardIterator<'a> { tree: &'a Tree, - current_node: Option<Index>, + current_node: Option<WrappedIndex>, include_self: bool, } @@ -707,8 +771,8 @@ impl<'a> UpwardIterator<'a> { /// /// /// [tree]: Tree - /// [node]: Index - pub fn new(tree: &'a Tree, starting_node: Option<Index>, include_self: bool) -> Self { + /// [node]: WrappedIndex + pub fn new(tree: &'a Tree, starting_node: Option<WrappedIndex>, include_self: bool) -> Self { Self { tree, current_node: starting_node, @@ -718,7 +782,7 @@ impl<'a> UpwardIterator<'a> { } impl<'a> Iterator for UpwardIterator<'a> { - type Item = Index; + type Item = WrappedIndex; fn next(&mut self) -> Option<Self::Item> { if self.include_self { @@ -733,11 +797,11 @@ impl<'a> Iterator for UpwardIterator<'a> { pub struct ChildIterator<'a> { pub tree: &'a Tree, - pub current_node: Option<Index>, + pub current_node: Option<WrappedIndex>, } impl<'a> Iterator for ChildIterator<'a> { - type Item = Index; + type Item = WrappedIndex; fn next(&mut self) -> Option<Self::Item> { if let Some(entity) = self.current_node { self.current_node = self.tree.get_next_sibling(entity); @@ -749,9 +813,9 @@ impl<'a> Iterator for ChildIterator<'a> { } impl<'a> Hierarchy<'a> for Tree { - type Item = Index; type DownIter = DownwardIterator<'a>; - type UpIter = Rev<std::vec::IntoIter<Index>>; + type UpIter = Rev<std::vec::IntoIter<WrappedIndex>>; + type Item = WrappedIndex; type ChildIter = ChildIterator<'a>; fn up_iter(&'a self) -> Self::UpIter { @@ -764,7 +828,7 @@ impl<'a> Hierarchy<'a> for Tree { DownwardIterator::new(self, self.root_node, true) } - fn child_iter(&'a self, node: Self::Item) -> Self::ChildIter { + fn child_iter(&'a self, node: WrappedIndex) -> Self::ChildIter { let first_child = self.get_first_child(node); ChildIterator { tree: self, @@ -772,7 +836,7 @@ impl<'a> Hierarchy<'a> for Tree { } } - fn parent(&self, node: Self::Item) -> Option<Self::Item> { + fn parent(&self, node: WrappedIndex) -> Option<WrappedIndex> { if let Some(parent_index) = self.parents.get(&node) { return Some(*parent_index); } @@ -780,7 +844,7 @@ impl<'a> Hierarchy<'a> for Tree { None } - fn is_first_child(&self, node: Self::Item) -> bool { + fn is_first_child(&self, node: WrappedIndex) -> bool { if let Some(parent) = self.parent(node) { if let Some(first_child) = self.get_first_child(parent) { if first_child == node { @@ -794,7 +858,7 @@ impl<'a> Hierarchy<'a> for Tree { false } - fn is_last_child(&self, node: Self::Item) -> bool { + fn is_last_child(&self, node: WrappedIndex) -> bool { if let Some(parent) = self.parent(node) { if let Some(parent_children) = self.children.get(&parent) { if let Some(last_child) = parent_children.last() { @@ -807,73 +871,11 @@ impl<'a> Hierarchy<'a> for Tree { } } -#[derive(Debug, Clone)] -pub struct WidgetTree { - tree: Arc<RwLock<Tree>>, -} - -impl WidgetTree { - pub fn new() -> Self { - Self { - tree: Arc::new(RwLock::new(Tree::default())), - } - } - - pub fn add(&self, index: Index, parent: Option<Index>) { - if let Ok(mut tree) = self.tree.write() { - tree.add(index, parent); - } - } - - pub fn take(self) -> Tree { - Arc::try_unwrap(self.tree).unwrap().into_inner().unwrap() - } -} - #[cfg(test)] mod tests { - use crate::node::NodeBuilder; use crate::tree::{DownwardIterator, UpwardIterator}; - use crate::{Arena, Index, Tree}; - - #[test] - fn test_tree() { - let mut store = Arena::new(); - let root = store.insert(NodeBuilder::empty().build()); - // Child 1 of root - let index1 = store.insert(NodeBuilder::empty().build()); - // Children of child 1. - let index2 = store.insert(NodeBuilder::empty().build()); - let index3 = store.insert(NodeBuilder::empty().build()); - // Child 2 of root - let index4 = store.insert(NodeBuilder::empty().build()); - - let mut tree = Tree::default(); - tree.root_node = Some(root); - - // Setup Parents.. - tree.parents.insert(index1, root); - tree.parents.insert(index4, root); - - tree.parents.insert(index2, index1); - tree.parents.insert(index3, index1); - - tree.children.insert(root, vec![index1, index4]); - tree.children.insert(index1, vec![index2, index3]); - - let flattened = tree.flatten(); - - let mapped = flattened - .iter() - .map(|x| x.into_raw_parts().0) - .collect::<Vec<_>>(); - - assert!(mapped[0] == 0); - assert!(mapped[1] == 1); - assert!(mapped[2] == 2); - assert!(mapped[3] == 3); - assert!(mapped[4] == 4); - } + use crate::tree::{Tree, WrappedIndex}; + use bevy::prelude::Entity; #[test] fn should_descend_tree() { @@ -885,13 +887,13 @@ mod tests { // D E F // G - let a = Index::from_raw_parts(0, 0); - let b = Index::from_raw_parts(1, 0); - let c = Index::from_raw_parts(2, 0); - let d = Index::from_raw_parts(3, 0); - let e = Index::from_raw_parts(4, 0); - let f = Index::from_raw_parts(5, 0); - let g = Index::from_raw_parts(6, 0); + let a = WrappedIndex(Entity::from_raw(0)); + let b = WrappedIndex(Entity::from_raw(1)); + let c = WrappedIndex(Entity::from_raw(2)); + let d = WrappedIndex(Entity::from_raw(3)); + let e = WrappedIndex(Entity::from_raw(4)); + let f = WrappedIndex(Entity::from_raw(5)); + let g = WrappedIndex(Entity::from_raw(6)); tree.add(a, None); tree.add(b, Some(a)); @@ -947,13 +949,13 @@ mod tests { // D E F // G - let a = Index::from_raw_parts(0, 0); - let b = Index::from_raw_parts(1, 0); - let c = Index::from_raw_parts(2, 0); - let d = Index::from_raw_parts(3, 0); - let e = Index::from_raw_parts(4, 0); - let f = Index::from_raw_parts(5, 0); - let g = Index::from_raw_parts(6, 0); + let a = WrappedIndex(Entity::from_raw(0)); + let b = WrappedIndex(Entity::from_raw(1)); + let c = WrappedIndex(Entity::from_raw(2)); + let d = WrappedIndex(Entity::from_raw(3)); + let e = WrappedIndex(Entity::from_raw(4)); + let f = WrappedIndex(Entity::from_raw(5)); + let g = WrappedIndex(Entity::from_raw(6)); tree.add(a, None); tree.add(b, Some(a)); @@ -1004,11 +1006,11 @@ mod tests { #[test] fn should_replace() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child_a = Index::from_raw_parts(1, 0); - let child_b = Index::from_raw_parts(2, 0); - let grandchild_a = Index::from_raw_parts(3, 0); - let grandchild_b = Index::from_raw_parts(4, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child_a = WrappedIndex(Entity::from_raw(1)); + let child_b = WrappedIndex(Entity::from_raw(2)); + let grandchild_a = WrappedIndex(Entity::from_raw(3)); + let grandchild_b = WrappedIndex(Entity::from_raw(4)); tree.add(root, None); tree.add(child_a, Some(root)); tree.add(child_b, Some(root)); @@ -1016,11 +1018,11 @@ mod tests { tree.add(grandchild_b, Some(child_b)); let mut expected = Tree::default(); - let expected_root = Index::from_raw_parts(5, 0); - let expected_child_a = Index::from_raw_parts(6, 0); - let expected_child_b = Index::from_raw_parts(7, 0); - let expected_grandchild_a = Index::from_raw_parts(8, 0); - let expected_grandchild_b = Index::from_raw_parts(9, 0); + let expected_root = WrappedIndex(Entity::from_raw(5)); + let expected_child_a = WrappedIndex(Entity::from_raw(6)); + let expected_child_b = WrappedIndex(Entity::from_raw(7)); + let expected_grandchild_a = WrappedIndex(Entity::from_raw(8)); + let expected_grandchild_b = WrappedIndex(Entity::from_raw(9)); expected.add(expected_root, None); expected.add(expected_child_a, Some(expected_root)); expected.add(expected_child_b, Some(expected_root)); @@ -1078,11 +1080,11 @@ mod tests { #[test] fn should_remove() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child_a = Index::from_raw_parts(1, 0); - let child_b = Index::from_raw_parts(2, 0); - let grandchild_a = Index::from_raw_parts(3, 0); - let grandchild_b = Index::from_raw_parts(4, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child_a = WrappedIndex(Entity::from_raw(1)); + let child_b = WrappedIndex(Entity::from_raw(2)); + let grandchild_a = WrappedIndex(Entity::from_raw(3)); + let grandchild_b = WrappedIndex(Entity::from_raw(4)); tree.add(root, None); tree.add(child_a, Some(root)); tree.add(child_b, Some(root)); @@ -1103,11 +1105,11 @@ mod tests { #[test] fn should_remove_root() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child_a = Index::from_raw_parts(1, 0); - let child_b = Index::from_raw_parts(2, 0); - let grandchild_a = Index::from_raw_parts(3, 0); - let grandchild_b = Index::from_raw_parts(4, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child_a = WrappedIndex(Entity::from_raw(1)); + let child_b = WrappedIndex(Entity::from_raw(2)); + let grandchild_a = WrappedIndex(Entity::from_raw(3)); + let grandchild_b = WrappedIndex(Entity::from_raw(4)); tree.add(root, None); tree.add(child_a, Some(root)); tree.add(child_b, Some(root)); @@ -1125,11 +1127,11 @@ mod tests { #[test] fn should_remove_and_reparent() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child_a = Index::from_raw_parts(1, 0); - let child_b = Index::from_raw_parts(2, 0); - let grandchild_a = Index::from_raw_parts(3, 0); - let grandchild_b = Index::from_raw_parts(4, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child_a = WrappedIndex(Entity::from_raw(1)); + let child_b = WrappedIndex(Entity::from_raw(2)); + let grandchild_a = WrappedIndex(Entity::from_raw(3)); + let grandchild_b = WrappedIndex(Entity::from_raw(4)); tree.add(root, None); tree.add(child_a, Some(root)); tree.add(child_b, Some(root)); @@ -1153,7 +1155,7 @@ mod tests { #[test] fn should_contain_root() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); + let root = WrappedIndex(Entity::from_raw(0)); tree.add(root, None); assert!(tree.contains(root)); @@ -1162,8 +1164,8 @@ mod tests { #[test] fn should_contain_child() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child = Index::from_raw_parts(1, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child = WrappedIndex(Entity::from_raw(1)); tree.add(root, None); tree.add(child, Some(root)); @@ -1175,16 +1177,16 @@ mod tests { fn should_be_empty() { let mut tree = Tree::default(); assert!(tree.is_empty()); - tree.add(Index::default(), None); + tree.add(WrappedIndex(Entity::from_raw(0)), None); assert!(!tree.is_empty()) } #[test] fn should_be_descendant() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child = Index::from_raw_parts(1, 0); - let grandchild = Index::from_raw_parts(2, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child = WrappedIndex(Entity::from_raw(1)); + let grandchild = WrappedIndex(Entity::from_raw(2)); tree.add(root, None); tree.add(child, Some(root)); tree.add(grandchild, Some(child)); @@ -1197,9 +1199,9 @@ mod tests { #[test] fn should_give_len() { let mut tree = Tree::default(); - let root = Index::from_raw_parts(0, 0); - let child = Index::from_raw_parts(1, 0); - let grandchild = Index::from_raw_parts(2, 0); + let root = WrappedIndex(Entity::from_raw(0)); + let child = WrappedIndex(Entity::from_raw(1)); + let grandchild = WrappedIndex(Entity::from_raw(2)); assert_eq!(0, tree.len()); tree.add(root, None); diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 0000000..5429f10 --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,7 @@ +use crate::context::WidgetName; + +pub trait Widget: Send + Sync { + fn get_name(&self) -> WidgetName { + WidgetName(std::any::type_name::<Self>().into()) + } +} diff --git a/src/widget_context.rs b/src/widget_context.rs new file mode 100644 index 0000000..a8ddd41 --- /dev/null +++ b/src/widget_context.rs @@ -0,0 +1,185 @@ +use std::sync::{Arc, RwLock}; + +use bevy::{prelude::Entity, utils::HashMap}; +use morphorm::Hierarchy; + +use crate::{ + context_entities::ContextEntities, layout::LayoutCache, node::WrappedIndex, prelude::Tree, +}; + +#[derive(Clone)] +pub struct WidgetContext { + old_tree: Arc<RwLock<Tree>>, + new_tree: Arc<RwLock<Tree>>, + context_entities: ContextEntities, + layout_cache: Arc<RwLock<LayoutCache>>, + index: Arc<RwLock<HashMap<Entity, usize>>>, +} + +impl WidgetContext { + pub(crate) fn new( + old_tree: Arc<RwLock<Tree>>, + context_entities: ContextEntities, + layout_cache: Arc<RwLock<LayoutCache>>, + ) -> Self { + Self { + old_tree, + new_tree: Arc::new(RwLock::new(Tree::default())), + context_entities, + layout_cache, + index: Arc::new(RwLock::new(HashMap::default())), + } + } + + pub(crate) fn store(&self, new_tree: &Tree) { + if let Ok(mut tree) = self.new_tree.write() { + *tree = new_tree.clone(); + } + } + + /// Creates a new context using the context entity for the given type_id + parent id. + pub fn set_context_entity<T: Default + 'static>( + &self, + parent_id: Option<Entity>, + context_entity: Entity, + ) { + if let Some(parent_id) = parent_id { + self.context_entities + .add_context_entity::<T>(parent_id, context_entity); + } + } + + /// Finds the closest matching context entity by traversing up the tree. + pub fn get_context_entity<T: Default + 'static>( + &self, + current_entity: Entity, + ) -> Option<Entity> { + // Check self first.. + if let Some(entity) = self + .context_entities + .get_context_entity::<T>(current_entity) + { + return Some(entity); + } + + // Check parents + if let Ok(tree) = self.old_tree.read() { + let mut parent = tree.get_parent(WrappedIndex(current_entity)); + while parent.is_some() { + if let Some(entity) = self + .context_entities + .get_context_entity::<T>(parent.unwrap().0) + { + return Some(entity); + } + parent = tree.get_parent(parent.unwrap()); + } + } + + None + } + + pub(crate) fn copy_from_point(&self, other_tree: &Arc<RwLock<Tree>>, entity: WrappedIndex) { + if let Ok(other_tree) = other_tree.read() { + if let Ok(mut tree) = self.new_tree.write() { + tree.copy_from_point(&other_tree, entity); + } + } + } + + pub fn clear_children(&self, entity: Entity) { + if let Ok(mut tree) = self.new_tree.write() { + tree.children.insert(WrappedIndex(entity), vec![]); + } + } + + pub fn get_children(&self, entity: Entity) -> Vec<Entity> { + let mut children = vec![]; + if let Ok(tree) = self.new_tree.read() { + let iterator = tree.child_iter(WrappedIndex(entity)); + + children = iterator.map(|index| index.0).collect::<Vec<_>>(); + } + + children + } + + fn get_children_old(&self, entity: Entity) -> Vec<Entity> { + let mut children = vec![]; + if let Ok(tree) = self.old_tree.read() { + let iterator = tree.child_iter(WrappedIndex(entity)); + + children = iterator.map(|index| index.0).collect::<Vec<_>>(); + } + + children + } + + fn get_and_add_index(&self, parent: Entity) -> usize { + if let Ok(mut hash_map) = self.index.try_write() { + if hash_map.contains_key(&parent) { + let index = hash_map.get_mut(&parent).unwrap(); + let current_index = index.clone(); + *index += 1; + return current_index; + } else { + hash_map.insert(parent, 1); + return 0; + } + } + + 0 + } + + pub fn get_child_at(&self, entity: Option<Entity>) -> Option<Entity> { + if let Some(entity) = entity { + let children = self.get_children_old(entity); + return children.get(self.get_and_add_index(entity)).cloned(); + } + None + } + + pub fn remove_children(&self, children_to_remove: Vec<Entity>) { + if let Ok(mut tree) = self.new_tree.write() { + for child in children_to_remove.iter() { + tree.remove(WrappedIndex(*child)); + } + } + } + + pub fn add_widget(&self, parent: Option<Entity>, entity: Entity) { + if let Ok(mut tree) = self.new_tree.write() { + tree.add( + WrappedIndex(entity), + parent.map(|parent| WrappedIndex(parent)), + ); + } + } + + /// Attempts to get the layout rect for the widget with the given ID + /// + /// # Arguments + /// + /// * `id`: The ID of the widget + /// + pub fn get_layout(&self, widget_id: Entity) -> Option<crate::layout::Rect> { + if let Ok(cache) = self.layout_cache.try_read() { + cache.rect.get(&WrappedIndex(widget_id)).cloned() + } else { + None + } + } + + pub fn dbg_tree(&self) { + if let Ok(tree) = self.new_tree.read() { + tree.dump() + } + } + + pub fn take(self) -> Tree { + Arc::try_unwrap(self.new_tree) + .unwrap() + .into_inner() + .unwrap() + } +} diff --git a/src/widgets/app.rs b/src/widgets/app.rs index 4d903a3..1853c47 100644 --- a/src/widgets/app.rs +++ b/src/widgets/app.rs @@ -1,81 +1,66 @@ -use kayak_core::OnLayout; +use bevy::{ + prelude::{Bundle, Commands, Component, Entity, In, Or, Query, Res, With}, + window::Windows, +}; +use morphorm::Units; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -use crate::widgets::Clip; +#[derive(Component, Default)] +pub struct KayakApp; -/// Props used by the [`App`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct AppProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, -} +impl Widget for KayakApp {} -#[widget] -/// The most common root widget -/// -/// # Props -/// -/// __Type:__ [`AppProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -/// # Using the `bevy_renderer` feature -/// -/// When the `bevy_renderer` feature is enabled, this widget will automatically bind to the window size -/// of the Bevy app. This allows it to update on window resize in order to match the width and height of the window. -pub fn App(props: AppProps) { - #[cfg(feature = "bevy_renderer")] - { - use crate::bevy::WindowSize; - use crate::core::styles::Units; - use crate::core::{Binding, Bound}; - let window_size = if let Ok(world) = context.get_global::<bevy::prelude::World>() { - if let Some(window_size) = world.get_resource::<Binding<WindowSize>>() { - window_size.clone() - } else { - return; - } - } else { - return; - }; +#[derive(Bundle)] +pub struct KayakAppBundle { + pub app: KayakApp, + pub styles: KStyle, + pub children: KChildren, + pub widget_name: WidgetName, +} - context.bind(&window_size); - let window_size = window_size.get(); - props.styles = Some( - Style::default() - .with_style(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - width: StyleProp::Value(Units::Pixels(window_size.0)), - height: StyleProp::Value(Units::Pixels(window_size.1)), - ..Default::default() - }) - .with_style(&props.styles), - ); +impl Default for KayakAppBundle { + fn default() -> Self { + Self { + app: Default::default(), + styles: Default::default(), + children: Default::default(), + widget_name: KayakApp::default().get_name(), + } } +} - rsx! { - <Clip> - {children} - </Clip> +/// TODO: USE CAMERA INSTEAD OF WINDOW!! +pub fn app_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + windows: Res<Windows>, + mut query: Query<(&mut KStyle, &KChildren), Or<(With<KayakApp>, With<Mounted>)>>, +) -> bool { + let mut has_changed = false; + let primary_window = windows.get_primary().unwrap(); + if let Ok((mut app_style, children)) = query.get_mut(entity) { + if app_style.width != StyleProp::Value(Units::Pixels(primary_window.width())) { + app_style.width = StyleProp::Value(Units::Pixels(primary_window.width())); + has_changed = true; + } + if app_style.height != StyleProp::Value(Units::Pixels(primary_window.height())) { + app_style.height = StyleProp::Value(Units::Pixels(primary_window.height())); + has_changed = true; + } + + app_style.render_command = StyleProp::Value(RenderCommand::Layout); + + if has_changed { + children.process(&widget_context, Some(entity)); + } } + + has_changed } diff --git a/src/widgets/background.rs b/src/widgets/background.rs index 3e91253..262bf61 100644 --- a/src/widgets/background.rs +++ b/src/widgets/background.rs @@ -1,49 +1,55 @@ -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Children, Fragment, OnEvent, WidgetProps, +use bevy::prelude::{Bundle, Changed, Commands, Component, Entity, In, Or, Query, With}; + +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + on_event::OnEvent, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -use kayak_core::OnLayout; -/// Props used by the [`Background`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct BackgroundProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, +#[derive(Component, Default)] +pub struct Background; + +impl Widget for Background {} + +#[derive(Bundle)] +pub struct BackgroundBundle { + pub background: Background, + pub styles: KStyle, + pub children: KChildren, + pub on_event: OnEvent, + pub widget_name: WidgetName, } -#[widget] -/// A widget that provides a simple, rectangular background -/// -/// # Props -/// -/// __Type:__ [`BackgroundProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Background(props: BackgroundProps) { - if props.styles.is_none() { - props.styles = Some(Style::default()) +impl Default for BackgroundBundle { + fn default() -> Self { + Self { + background: Default::default(), + styles: Default::default(), + children: Default::default(), + on_event: Default::default(), + widget_name: Background::default().get_name(), + } } - props.styles.as_mut().unwrap().render_command = StyleProp::Value(RenderCommand::Quad); - rsx! { - <Fragment> - {children} - </Fragment> +} + +pub fn update_background( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query< + (&mut KStyle, &KChildren), + Or<( + (Changed<KStyle>, Changed<KChildren>, With<Background>), + With<Mounted>, + )>, + >, +) -> bool { + if let Ok((mut style, children)) = query.get_mut(entity) { + style.render_command = StyleProp::Value(RenderCommand::Quad); + children.process(&widget_context, Some(entity)); + return true; } + false } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 0c89c7b..c0e0fc6 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,94 +1,71 @@ -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Corner, Style, StyleProp, Units}, - widget, Children, Color, Fragment, OnEvent, OnLayout, WidgetProps, +use bevy::{ + prelude::{Bundle, Changed, Color, Commands, Component, Entity, In, Or, Query, With}, + window::CursorIcon, }; -use kayak_core::CursorIcon; -/// Props used by the [`Button`] widget -#[derive(Default, Debug, PartialEq, Clone)] -pub struct ButtonProps { - /// If true, disables this widget not allowing it to be focusable - /// - // TODO: Update this documentation when the disabled issue is fixed - /// Currently, this does not actually disable the button from being clicked. - pub disabled: bool, - pub styles: Option<Style>, - pub children: Option<Children>, - pub on_event: Option<OnEvent>, - pub on_layout: Option<OnLayout>, - pub focusable: Option<bool>, -} - -impl WidgetProps for ButtonProps { - fn get_children(&self) -> Option<Children> { - self.children.clone() - } - - fn set_children(&mut self, children: Option<Children>) { - self.children = children; - } +use crate::{ + context::{Mounted, WidgetName}, + on_event::OnEvent, + prelude::{KChildren, Units, WidgetContext}, + styles::{Corner, KCursorIcon, KStyle, RenderCommand, StyleProp}, + widget::Widget, +}; - fn get_styles(&self) -> Option<Style> { - self.styles.clone() - } +#[derive(Component, Default)] +pub struct KButton; - fn get_on_event(&self) -> Option<OnEvent> { - self.on_event.clone() - } - - fn get_on_layout(&self) -> Option<OnLayout> { - self.on_layout.clone() - } +#[derive(Bundle)] +pub struct KButtonBundle { + pub button: KButton, + pub styles: KStyle, + pub on_event: OnEvent, + pub children: KChildren, + pub widget_name: WidgetName, +} - fn get_focusable(&self) -> Option<bool> { - Some(!self.disabled) +impl Default for KButtonBundle { + fn default() -> Self { + Self { + button: Default::default(), + styles: Default::default(), + on_event: Default::default(), + children: KChildren::default(), + widget_name: KButton::default().get_name(), + } } } -#[widget] -/// A widget that is styled like a button -/// -/// # Props -/// -/// __Type:__ [`ButtonProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Button(props: ButtonProps) { - // TODO: This should probably do more than just provide basic styling. - // Ideally, we could add a `Handler` prop for `on_click` and other common cursor - // events. Giving it the additional purpose of being a compact way to define a button. - // This also allows us to make `disable` trule disable the button. - // Also, styles need to reflect disabled status. - props.styles = Some( - Style::default() - .with_style(Style { +impl Widget for KButton {} + +pub fn button_update( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query<(&mut KStyle, &KChildren), Or<(Changed<KButton>, With<Mounted>)>>, +) -> bool { + if let Ok((mut style, children)) = query.get_mut(entity) { + *style = KStyle::default() + .with_style(KStyle { render_command: StyleProp::Value(RenderCommand::Quad), ..Default::default() }) - .with_style(&props.styles) - .with_style(Style { - background_color: StyleProp::Value(Color::new(0.0781, 0.0898, 0.101, 1.0)), + .with_style(style.clone()) + .with_style(KStyle { + render_command: StyleProp::Value(RenderCommand::Quad), + background_color: StyleProp::Value(Color::rgba(0.0781, 0.0898, 0.101, 1.0)), border_radius: StyleProp::Value(Corner::all(5.0)), height: StyleProp::Value(Units::Pixels(45.0)), padding_left: StyleProp::Value(Units::Stretch(1.0)), padding_right: StyleProp::Value(Units::Stretch(1.0)), - cursor: CursorIcon::Hand.into(), + padding_bottom: StyleProp::Value(Units::Stretch(1.0)), + padding_top: StyleProp::Value(Units::Stretch(1.0)), + cursor: StyleProp::Value(KCursorIcon(CursorIcon::Hand)), ..Default::default() - }), - ); + }); + + children.process(&widget_context, Some(entity)); - rsx! { - <Fragment> - {children} - </Fragment> + return true; } + + false } diff --git a/src/widgets/clip.rs b/src/widgets/clip.rs index 8f0b115..c07bcdf 100644 --- a/src/widgets/clip.rs +++ b/src/widgets/clip.rs @@ -1,62 +1,50 @@ -use kayak_core::OnLayout; +use bevy::prelude::{Bundle, Changed, Commands, Component, Entity, In, Or, Query, With}; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp, Units}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp, Units}, + widget::Widget, }; -/// Props used by the [`Clip`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct ClipProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, +#[derive(Component, Default)] +pub struct Clip; + +impl Widget for Clip {} + +#[derive(Bundle)] +pub struct ClipBundle { + pub clip: Clip, + pub styles: KStyle, + pub children: KChildren, + pub widget_name: WidgetName, +} + +impl Default for ClipBundle { + fn default() -> Self { + Self { + clip: Clip::default(), + styles: KStyle { + render_command: StyleProp::Value(RenderCommand::Clip), + height: StyleProp::Value(Units::Stretch(1.0)), + width: StyleProp::Value(Units::Stretch(1.0)), + ..KStyle::default() + }, + children: KChildren::default(), + widget_name: Clip::default().get_name(), + } + } } -#[widget] -/// A widget that clips its contents to fit the parent container or its designated -/// [`width`](Style::width) and [`height`](Style::height) styling -/// -/// # Props -/// -/// __Type:__ [`ClipProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ⌠| -/// -pub fn Clip(props: ClipProps) { - let incoming_styles = props.styles.clone().unwrap_or_default(); - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Clip), - width: if matches!(incoming_styles.width, StyleProp::Value(..)) { - incoming_styles.width - } else { - StyleProp::Value(Units::Stretch(1.0)) - }, - height: if matches!(incoming_styles.height, StyleProp::Value(..)) { - incoming_styles.height - } else { - StyleProp::Value(Units::Stretch(1.0)) - }, - // min_width: StyleProp::Value(Units::Stretch(1.0)), - // min_height: StyleProp::Value(Units::Stretch(1.0)), - ..incoming_styles - }); - rsx! { - <> - {children} - </> +pub fn update_clip( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query<(&KStyle, &KChildren), Or<((Changed<KStyle>, With<Clip>), With<Mounted>)>>, +) -> bool { + if let Ok((_, children)) = query.get_mut(entity) { + children.process(&widget_context, Some(entity)); + return true; } + false } diff --git a/src/widgets/element.rs b/src/widgets/element.rs index 3797a1b..5bfd3cb 100644 --- a/src/widgets/element.rs +++ b/src/widgets/element.rs @@ -1,53 +1,57 @@ -use kayak_core::OnLayout; +use bevy::prelude::{Bundle, Changed, Commands, Component, Entity, In, Or, Query, With}; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + on_event::OnEvent, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -/// Props used by the [`Element`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct ElementProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, +#[derive(Component, Default)] +pub struct Element; + +impl Widget for Element {} + +#[derive(Bundle)] +pub struct ElementBundle { + pub element: Element, + pub styles: KStyle, + pub on_event: OnEvent, + pub children: KChildren, + pub widget_name: WidgetName, } -#[widget] -/// The most basic widget, equivalent to `div` from HTML. -/// -/// It essentially just sets the [`RenderCommand`] of this widget to [`RenderCommand::Layout`]. -/// -/// # Props -/// -/// __Type:__ [`ElementProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Element(props: ElementProps) { - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - ..props.styles.clone().unwrap_or_default() - }); +impl Default for ElementBundle { + fn default() -> Self { + Self { + element: Default::default(), + styles: Default::default(), + children: Default::default(), + on_event: OnEvent::default(), + widget_name: Element::default().get_name(), + } + } +} - rsx! { - <> - {children} - </> +pub fn update_element( + In((mut widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query< + (&mut KStyle, &KChildren), + Or<((Changed<KStyle>, With<Element>), With<Mounted>)>, + >, +) -> bool { + if let Ok((mut style, children)) = query.get_mut(entity) { + *style = KStyle::default() + .with_style(style.clone()) + .with_style(KStyle { + render_command: StyleProp::Value(RenderCommand::Layout), + ..Default::default() + }); + children.process(&mut widget_context, Some(entity)); + return true; } + false } diff --git a/src/widgets/fold.rs b/src/widgets/fold.rs deleted file mode 100644 index 99fba89..0000000 --- a/src/widgets/fold.rs +++ /dev/null @@ -1,141 +0,0 @@ -use kayak_core::OnLayout; - -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp, Units}, - use_state, widget, Children, EventType, Handler, OnEvent, WidgetProps, -}; - -use crate::widgets::{Background, Clip, If, Text}; - -// TODO: Add `disabled` prop - -/// Props used by the [`Fold`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct FoldProps { - /// The initial open state of the fold - pub default_open: bool, - /// The string displayed as the label of this fold element - pub label: String, - /// A callback for when the user presses the fold's label - /// - /// The handler is given the boolean value of the desired open state. For example, - /// if the fold is closed and the user presses on the label, this callback will be - /// fired with the boolean value `true`. - pub on_change: Option<Handler<bool>>, - /// Sets the controlled open state of the fold - /// - /// If `None`, the open state will be automatically handled internally. - /// This is useful for if you don't need or care to manage the toggling yourself. - pub open: Option<bool>, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, -} - -#[widget] -/// A widget container that toggles its content between visible and hidden when clicked -/// -/// # Props -/// -/// __Type:__ [`FoldProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -/// # Examples -/// -/// ``` -/// # use kayak_ui::core::{Handler, rsx, use_state}; -/// # use kayak_ui::widgets::{Text}; -/// -/// let (open, set_open) = use_state!(false); -/// let on_change = Handler::new(move |value| { -/// set_open(value); -/// }); -/// -/// rsx! { -/// <Fold label={"Toggle Open/Close".to_string()} open={open} on_change={Some(on_change)}> -/// <Text content={"Fold Content".to_string()} size={16.0} /> -/// </Fold> -/// } -/// ``` -pub fn Fold(props: FoldProps) { - let FoldProps { - default_open, - label, - on_change, - open, - .. - } = props.clone(); - - // === State === // - let initial = default_open || open.unwrap_or_default(); - let (is_open, set_is_open, ..) = use_state!(initial); - if let Some(open) = open { - // This is a controlled state - set_is_open(open); - } - - let handler = OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => { - if open.is_none() { - // This is an internally-managed state - set_is_open(!is_open); - } - if let Some(ref callback) = on_change { - callback.call(!is_open); - } - } - _ => {} - }); - - // === Styles === // - props.styles = Some(Style { - height: StyleProp::Value(Units::Auto), - render_command: StyleProp::Value(RenderCommand::Layout), - ..props.styles.clone().unwrap_or_default() - }); - - let background_styles = Style { - background_color: StyleProp::Inherit, - border_radius: StyleProp::Inherit, - height: StyleProp::Value(Units::Auto), - ..Default::default() - }; - - let container_style = Style { - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Auto), - ..Default::default() - }; - - let inner_container_styles = container_style.clone(); - - // === Render === // - rsx! { - <Background styles={Some(background_styles)}> - <Clip styles={Some(container_style)}> - <Text content={label} on_event={Some(handler)} size={14.0} /> - <If condition={is_open}> - <Clip styles={Some(inner_container_styles)}> - {children} - </Clip> - </If> - </Clip> - </Background> - } -} diff --git a/src/widgets/if_element.rs b/src/widgets/if_element.rs deleted file mode 100644 index 9a3b2ce..0000000 --- a/src/widgets/if_element.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::core::{rsx, styles::Style, widget, Children, OnEvent, WidgetProps}; - -/// Props used by the [`If`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct IfProps { - /// If true, the children will be rendered, otherwise nothing will be rendered - pub condition: bool, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, -} - -#[widget] -/// A widget that _conditionally_ renders its children -/// -/// # Props -/// -/// __Type:__ [`IfProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ⌠| -/// | `focusable` | ✅ | -/// -pub fn If(props: IfProps) { - if props.condition { - rsx! { - <> - {children} - </> - } - } -} diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 5c3c0b8..6968c24 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -1,54 +1,43 @@ -use kayak_core::OnLayout; +use bevy::prelude::{Bundle, Changed, Component, Entity, Handle, In, Or, Query, With}; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -/// Props used by the [`Image`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct ImageProps { - pub handle: u16, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, +#[derive(Component, Default)] +pub struct Image(pub Handle<bevy::prelude::Image>); + +impl Widget for Image {} + +#[derive(Bundle)] +pub struct ImageBundle { + pub image: Image, + pub style: KStyle, + pub widget_name: WidgetName, } -#[widget] -/// A widget that renders an image background -/// -/// # Props -/// -/// __Type:__ [`ImageProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Image(props: ImageProps) { - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Image { - handle: props.handle, - }), - ..props.styles.clone().unwrap_or_default() - }); +impl Default for ImageBundle { + fn default() -> Self { + Self { + image: Default::default(), + style: Default::default(), + widget_name: Image::default().get_name(), + } + } +} - rsx! { - <> - {children} - </> +pub fn update_image( + In((_widget_context, entity)): In<(WidgetContext, Entity)>, + mut query: Query<(&mut KStyle, &Image), Or<((Changed<Image>, Changed<KStyle>), With<Mounted>)>>, +) -> bool { + if let Ok((mut style, image)) = query.get_mut(entity) { + style.render_command = StyleProp::Value(RenderCommand::Image { + handle: image.0.clone_weak(), + }); + return true; } + false } diff --git a/src/widgets/inspector.rs b/src/widgets/inspector.rs deleted file mode 100644 index 33b905a..0000000 --- a/src/widgets/inspector.rs +++ /dev/null @@ -1,114 +0,0 @@ -use kayak_core::styles::{Corner, PositionType, Style, StyleProp, Units}; -use kayak_core::{Bound, Color, EventType, OnEvent, VecTracker}; -use kayak_render_macros::{constructor, use_state}; - -use crate::core::{rsx, widget, MutableBound, WidgetProps}; - -use crate::widgets::{Background, Button, Text}; - -// TODO: Remove if unneeded -#[derive(Clone, PartialEq)] -pub enum InspectData { - None, - Data(Vec<String>), -} - -/// Props used by the [`Inspector`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct InspectorProps { - #[prop_field(Styles)] - pub styles: Option<Style>, -} - -#[widget] -/// A widget that displays debug data for inspected widgets -/// -/// "Inspected widgets" refers to the last clicked widget. -/// -/// # Props -/// -/// __Type:__ [`InspectorProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ⌠| -/// | `styles` | ✅ | -/// | `on_event` | ⌠| -/// | `on_layout` | ⌠| -/// | `focusable` | ⌠| -/// -pub fn Inspector(props: InspectorProps) { - let (inspect_data, set_inspect_data, _) = use_state!(Vec::<String>::new()); - - let background_styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.125, 0.125, 0.125, 1.0)), - border_radius: StyleProp::Value(Corner::all(0.0)), - position_type: StyleProp::Value(PositionType::SelfDirected), - left: StyleProp::Value(Units::Stretch(1.0)), - top: StyleProp::Value(Units::Stretch(0.0)), - bottom: StyleProp::Value(Units::Stretch(0.0)), - width: StyleProp::Value(Units::Pixels(200.0)), - ..props.styles.clone().unwrap_or_default() - }); - - let last_clicked = context.get_last_clicked_widget(); - context.bind(&last_clicked); - - let last_clicked_value = last_clicked.get(); - let (id, _) = last_clicked_value.into_raw_parts(); - - let mut parent_id_move = None; - if let Some(layout) = context.get_layout(&last_clicked_value) { - if let Some(node) = context.get_node(&last_clicked_value) { - let mut data = Vec::new(); - - if let Some(name) = context.get_name(&last_clicked_value) { - data.push(format!("Name: {}", name)); - } - - data.push(format!("ID: {}", id)); - data.push(format!("X: {}", layout.posx)); - data.push(format!("Y: {}", layout.posy)); - data.push(format!("Width: {}", layout.width)); - data.push(format!("Height: {}", layout.height)); - data.push(format!( - "RenderCommand: \n{:#?}", - node.resolved_styles.render_command - )); - data.push(format!("Height: \n{:#?}", node.resolved_styles.height)); - - if let Some(parent_id) = context.get_valid_parent(last_clicked_value) { - parent_id_move = Some(parent_id); - if let Some(layout) = context.get_layout(&parent_id) { - data.push(format!("_________Parent_________")); - if let Some(name) = context.get_name(&parent_id) { - data.push(format!("Name: {}", name)); - } - data.push(format!("X: {}", layout.posx)); - data.push(format!("Y: {}", layout.posy)); - data.push(format!("Width: {}", layout.width)); - data.push(format!("Height: {}", layout.height)); - } - } - set_inspect_data(data); - } - } - - let handle_button_events = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::Click(..) => last_clicked.set(parent_id_move.unwrap()), - _ => {} - })); - - rsx! { - <Background styles={background_styles}> - {VecTracker::from(inspect_data.iter().map(|data| { - constructor! { - <Text content={data.clone().to_string()} size={12.0} /> - } - }))} - <Button> - <Text content={"Go Up".into()} size={12.0} on_event={handle_button_events} /> - </Button> - </Background> - } -} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 800db81..fab668a 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,37 +1,84 @@ +use bevy::prelude::*; + mod app; mod background; mod button; mod clip; mod element; -mod fold; -mod if_element; mod image; -mod inspector; mod nine_patch; -mod on_change; mod scroll; -mod spin_box; mod text; mod text_box; mod texture_atlas; -mod tooltip; mod window; -pub use app::*; -pub use background::*; -pub use button::*; -pub use clip::*; -pub use element::*; -pub use fold::*; -pub use if_element::*; -pub use image::*; -pub use inspector::*; -pub use nine_patch::*; -pub use on_change::*; -pub use scroll::*; -pub use spin_box::*; -pub use text::*; -pub use text_box::*; -pub use texture_atlas::*; -pub use tooltip::*; -pub use window::*; +pub use app::{KayakApp, KayakAppBundle}; +pub use background::{Background, BackgroundBundle}; +pub use button::{KButton, KButtonBundle}; +pub use clip::{Clip, ClipBundle}; +pub use element::{Element, ElementBundle}; +pub use image::{Image, ImageBundle}; +pub use nine_patch::{NinePatch, NinePatchBundle}; +pub use scroll::{ + scroll_bar::{ScrollBarBundle, ScrollBarProps}, + scroll_box::{ScrollBoxBundle, ScrollBoxProps}, + scroll_content::{ScrollContentBundle, ScrollContentProps}, + scroll_context::{ + ScrollContext, ScrollContextProvider, ScrollContextProviderBundle, ScrollMode, + }, +}; +pub use text::{TextProps, TextWidgetBundle}; +pub use text_box::{TextBoxBundle, TextBoxProps}; +pub use texture_atlas::{TextureAtlas, TextureAtlasBundle}; +pub use window::{KWindow, WindowBundle}; + +use app::app_update; +use background::update_background; +use button::button_update; +use clip::update_clip; +use element::update_element; +use image::update_image; +use nine_patch::update_nine_patch; +use scroll::{ + scroll_bar::update_scroll_bar, scroll_box::update_scroll_box, + scroll_content::update_scroll_content, scroll_context::update_scroll_context, +}; +use text::text_update; +use text_box::update_text_box; +use texture_atlas::update_texture_atlas; +use window::window_update; + +use crate::{context::Context, widget::Widget}; + +pub struct KayakWidgets; + +impl Plugin for KayakWidgets { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_startup_system_to_stage(StartupStage::PostStartup, add_widget_systems); + } +} + +fn add_widget_systems(mut context: ResMut<Context>) { + context.add_widget_system(KayakApp::default().get_name(), app_update); + context.add_widget_system(KButton::default().get_name(), button_update); + context.add_widget_system(TextProps::default().get_name(), text_update); + context.add_widget_system(KWindow::default().get_name(), window_update); + context.add_widget_system(Background::default().get_name(), update_background); + context.add_widget_system(Clip::default().get_name(), update_clip); + context.add_widget_system(Image::default().get_name(), update_image); + context.add_widget_system(TextureAtlas::default().get_name(), update_texture_atlas); + context.add_widget_system(NinePatch::default().get_name(), update_nine_patch); + context.add_widget_system(Element::default().get_name(), update_element); + context.add_widget_system(ScrollBarProps::default().get_name(), update_scroll_bar); + context.add_widget_system( + ScrollContentProps::default().get_name(), + update_scroll_content, + ); + context.add_widget_system(ScrollBoxProps::default().get_name(), update_scroll_box); + context.add_widget_system( + ScrollContextProvider::default().get_name(), + update_scroll_context, + ); + context.add_widget_system(TextBoxProps::default().get_name(), update_text_box); +} diff --git a/src/widgets/nine_patch.rs b/src/widgets/nine_patch.rs index 4e2e68d..d368a66 100644 --- a/src/widgets/nine_patch.rs +++ b/src/widgets/nine_patch.rs @@ -1,72 +1,61 @@ -use kayak_core::OnLayout; +use bevy::prelude::{ + Bundle, Changed, Commands, Component, Entity, Handle, Image, In, Or, Query, With, +}; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Edge, Style, StyleProp}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{Edge, KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -/// Props used by the [`NinePatch`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct NinePatchProps { +#[derive(Component, Default, Debug)] +pub struct NinePatch { /// The handle to image - pub handle: u16, + pub handle: Handle<Image>, /// The size of each edge (in pixels) pub border: Edge<f32>, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, } -#[widget] -/// A widget that renders a nine-patch image background -/// -/// A nine-patch is a special type of image that's broken into nine parts: -/// -/// * Edges - Top, Bottom, Left, Right -/// * Corners - Top-Left, Top-Right, Bottom-Left, Bottom-Right -/// * Center -/// -/// Using these parts of an image, we can construct a scalable background and border -/// all from a single image. This is done by: -/// -/// * Stretching the edges (vertically for left/right and horizontally for top/bottom) -/// * Preserving the corners -/// * Scaling the center to fill the remaining space -/// -/// -/// # Props -/// -/// __Type:__ [`NinePatchProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn NinePatch(props: NinePatchProps) { - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::NinePatch { - handle: props.handle, - border: props.border, - }), - ..props.styles.clone().unwrap_or_default() - }); +impl Widget for NinePatch {} + +#[derive(Bundle)] +pub struct NinePatchBundle { + pub nine_patch: NinePatch, + pub styles: KStyle, + pub children: KChildren, + pub widget_name: WidgetName, +} + +impl Default for NinePatchBundle { + fn default() -> Self { + Self { + nine_patch: Default::default(), + styles: Default::default(), + children: KChildren::default(), + widget_name: NinePatch::default().get_name(), + } + } +} + +pub fn update_nine_patch( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + _: Commands, + mut query: Query< + (&mut KStyle, &NinePatch, &KChildren), + Or<((Changed<NinePatch>, Changed<KStyle>), With<Mounted>)>, + >, +) -> bool { + if let Ok((mut style, nine_patch, children)) = query.get_mut(entity) { + style.render_command = StyleProp::Value(RenderCommand::NinePatch { + border: nine_patch.border, + handle: nine_patch.handle.clone_weak(), + }); + + children.process(&widget_context, Some(entity)); - rsx! { - <> - {children} - </> + return true; } + false } diff --git a/src/widgets/on_change.rs b/src/widgets/on_change.rs deleted file mode 100644 index b4fdebb..0000000 --- a/src/widgets/on_change.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::sync::{Arc, RwLock}; - -#[derive(Debug, Clone, PartialEq)] -pub struct ChangeEvent { - pub value: String, -} - -#[derive(Clone)] -pub struct OnChange(pub Arc<RwLock<dyn FnMut(ChangeEvent) + Send + Sync + 'static>>); - -impl OnChange { - pub fn new<F: FnMut(ChangeEvent) + Send + Sync + 'static>(f: F) -> OnChange { - OnChange(Arc::new(RwLock::new(f))) - } -} - -impl PartialEq for OnChange { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -impl std::fmt::Debug for OnChange { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("OnChange").finish() - } -} diff --git a/src/widgets/scroll/mod.rs b/src/widgets/scroll/mod.rs index 268ab20..e07a048 100644 --- a/src/widgets/scroll/mod.rs +++ b/src/widgets/scroll/mod.rs @@ -1,12 +1,7 @@ -mod scroll_bar; -mod scroll_box; -mod scroll_content; -mod scroll_context; - -pub use scroll_bar::*; -pub use scroll_box::*; -use scroll_content::*; -pub use scroll_context::*; +pub mod scroll_bar; +pub mod scroll_box; +pub mod scroll_content; +pub mod scroll_context; /// Maps a value from one range to another range fn map_range(value: f32, from_range: (f32, f32), to_range: (f32, f32)) -> f32 { diff --git a/src/widgets/scroll/scroll_bar.rs b/src/widgets/scroll/scroll_bar.rs index f7ac4bc..8748945 100644 --- a/src/widgets/scroll/scroll_bar.rs +++ b/src/widgets/scroll/scroll_bar.rs @@ -1,19 +1,23 @@ -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{PositionType, Style, Units}, - use_state, widget, Bound, EventType, MutableBound, OnEvent, WidgetProps, +use bevy::prelude::{ + Bundle, Changed, Color, Commands, Component, Entity, In, Or, ParamSet, Query, With, }; -use kayak_core::layout_cache::Rect; -use kayak_core::styles::{Corner, Edge}; -use kayak_core::Color; +use kayak_ui_macros::rsx; -use crate::widgets::{Background, Clip}; +use crate::{ + context::{Mounted, WidgetName}, + event::{Event, EventType}, + event_dispatcher::EventDispatcherContext, + on_event::OnEvent, + prelude::{KChildren, WidgetContext}, + styles::{Corner, Edge, KStyle, PositionType, RenderCommand, Units}, + widget::Widget, + widgets::{BackgroundBundle, ClipBundle}, +}; -use super::{map_range, ScrollContext}; +use super::{map_range, scroll_context::ScrollContext}; /// Props used by the [`ScrollBar`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] +#[derive(Component, Default, Debug, PartialEq, Clone)] pub struct ScrollBarProps { /// If true, disables the ability to drag pub disabled: bool, @@ -24,282 +28,293 @@ pub struct ScrollBarProps { /// The color of the scrollbar thumb pub thumb_color: Option<Color>, /// The styles of the scrollbar thumb - pub thumb_styles: Option<Style>, + pub thumb_styles: Option<KStyle>, /// The color of the scrollbar track pub track_color: Option<Color>, /// The styles of the scrollbar track - pub track_styles: Option<Style>, - #[prop_field(Styles)] - styles: Option<Style>, + pub track_styles: Option<KStyle>, } -#[widget] -/// A widget that displays the current scroll progress within a [`ScrollBox`](crate::ScrollBox) widget -/// -/// This widget consists of two main components: -/// -/// ### The Thumb -/// -/// This is the actual indicator for scroll progress. It also allows you to control the -/// scroll offset by dragging it around. -/// -/// ### The Track -/// -/// This is the track along which the thumb moves. Clicking anywhere in the track will move -/// the thumb to the clicked position and update the scroll offset. -/// -/// # Props -/// -/// __Type:__ [`ScrollBarProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ⌠| -/// | `styles` | ✅ | -/// | `on_event` | ⌠| -/// | `focusable` | ⌠| -/// -/// # Panics -/// -/// This widget will panic when used outside the context of a [`ScrollContext`], which -/// is automatically provided by [`ScrollBox`](crate::ScrollBox). -pub fn ScrollBar(props: ScrollBarProps) { - // === Scroll === // - let scroll_ctx = context.create_consumer::<ScrollContext>().unwrap(); - let scroll: ScrollContext = scroll_ctx.get(); - let scroll_x = scroll.scroll_x(); - let scroll_y = scroll.scroll_y(); - let content_width = scroll.content_width(); - let content_height = scroll.content_height(); - let scrollable_width = scroll.scrollable_width(); - let scrollable_height = scroll.scrollable_height(); +impl Widget for ScrollBarProps {} - // === Layout === // - let id = self.get_id(); - let layout = if let Some(layout) = context.get_layout(&id) { - *layout - } else { - Rect::default() - }; +#[derive(Bundle)] +pub struct ScrollBarBundle { + pub scrollbar_props: ScrollBarProps, + pub styles: KStyle, + pub widget_name: WidgetName, +} - // === Configuration === // - let disabled = props.disabled; - let horizontal = props.horizontal; - let _thickness = props.thickness; - let thickness = props.thickness; - let thumb_color = props - .thumb_color - .unwrap_or_else(|| Color::new(0.2981, 0.3098, 0.321, 0.95)); - let thumb_styles = props.thumb_styles.clone(); - let track_color = props - .track_color - .unwrap_or_else(|| Color::new(0.1581, 0.1758, 0.191, 0.15)); - let track_styles = props.track_styles.clone(); - // The size of the thumb as a percentage - let thumb_size_percent = (if props.horizontal { - layout.width / (content_width - thickness).max(1.0) - } else { - layout.height / (content_height - thickness).max(1.0) - }) - .clamp(0.1, 1.0); - // The size of the thumb in pixels - let thumb_size_pixels = thumb_size_percent - * if props.horizontal { - layout.width - } else { - layout.height - }; - let thumb_extents = thumb_size_pixels / 2.0; - let percent_scrolled = if props.horizontal { - scroll.percent_x() - } else { - scroll.percent_y() - }; - // The thumb's offset as a percentage - let thumb_offset = map_range( - percent_scrolled * 100.0, - (0.0, 100.0), - (0.0, 100.0 - thumb_size_percent * 100.0), - ); +impl Default for ScrollBarBundle { + fn default() -> Self { + Self { + scrollbar_props: Default::default(), + styles: Default::default(), + widget_name: ScrollBarProps::default().get_name(), + } + } +} - // === Styles === // - props.styles = Some( - Style::default().with_style(Style { - render_command: RenderCommand::Layout.into(), - width: if horizontal { - Units::Stretch(1.0) - } else { - Units::Pixels(thickness) - } - .into(), - height: if horizontal { - Units::Pixels(thickness) - } else { - Units::Stretch(1.0) - } - .into(), - ..Default::default() - }), - ); +pub fn update_scroll_bar( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: ParamSet<( + Query<Entity, Or<(Changed<ScrollBarProps>, With<Mounted>)>>, + Query<(&ScrollBarProps, &mut KStyle)>, + )>, + mut context_query: ParamSet<(Query<Entity, Changed<ScrollContext>>, Query<&ScrollContext>)>, +) -> bool { + if !context_query.p0().is_empty() | !query.p0().is_empty() { + if let Ok((scrollbar, mut styles)) = query.p1().get_mut(entity) { + if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) + { + if let Ok(scroll_context) = context_query.p1().get(context_entity) { + let scroll_x = scroll_context.scroll_x(); + let scroll_y = scroll_context.scroll_y(); + let content_width = scroll_context.content_width(); + let content_height = scroll_context.content_height(); + let scrollable_width = scroll_context.scrollable_width(); + let scrollable_height = scroll_context.scrollable_height(); - let mut track_style = Style::default() - .with_style(&track_styles) - .with_style(Style { - background_color: track_color.into(), - border_radius: Corner::all(thickness / 2.0).into(), - ..Default::default() - }); + let layout = widget_context.get_layout(entity).unwrap_or_default(); - let mut border_color = thumb_color; - border_color.a = (border_color.a - 0.2).max(0.0); - border_color.r = (border_color.r + 0.1).min(1.0); - border_color.g = (border_color.g + 0.1).min(1.0); - border_color.b = (border_color.b + 0.1).min(1.0); + // === Configuration === // + // let disabled = scrollbar.disabled; + let horizontal = scrollbar.horizontal; + let _thickness = scrollbar.thickness; + let thickness = scrollbar.thickness; + let thumb_color = scrollbar + .thumb_color + .unwrap_or_else(|| Color::rgba(0.2981, 0.3098, 0.321, 0.95)); + let thumb_styles = scrollbar.thumb_styles.clone(); + let track_color = scrollbar + .track_color + .unwrap_or_else(|| Color::rgba(0.1581, 0.1758, 0.191, 0.15)); + let track_styles = scrollbar.track_styles.clone(); + // The size of the thumb as a percentage + let thumb_size_percent = (if scrollbar.horizontal { + layout.width / (content_width - thickness).max(1.0) + } else { + layout.height / (content_height - thickness).max(1.0) + }) + .clamp(0.1, 1.0); + // The size of the thumb in pixels + let thumb_size_pixels = thumb_size_percent + * if scrollbar.horizontal { + layout.width + } else { + layout.height + }; + let thumb_extents = thumb_size_pixels / 2.0; + let percent_scrolled = if scrollbar.horizontal { + scroll_context.percent_x() + } else { + scroll_context.percent_y() + }; + // The thumb's offset as a percentage + let thumb_offset = map_range( + percent_scrolled * 100.0, + (0.0, 100.0), + (0.0, 100.0 - thumb_size_percent * 100.0), + ); - let mut thumb_style = Style::default() - .with_style(Style { - position_type: PositionType::SelfDirected.into(), - ..Default::default() - }) - .with_style(&thumb_styles) - .with_style(Style { - background_color: thumb_color.into(), - border_radius: Corner::all(thickness / 2.0).into(), - border: Edge::all(1.0).into(), - border_color: border_color.into(), - ..Default::default() - }); + // === Styles === // + *styles = KStyle::default().with_style(KStyle { + render_command: RenderCommand::Layout.into(), + width: if horizontal { + Units::Stretch(1.0) + } else { + Units::Pixels(thickness) + } + .into(), + height: if horizontal { + Units::Pixels(thickness) + } else { + Units::Stretch(1.0) + } + .into(), + ..Default::default() + }); - if props.horizontal { - track_style.apply(Style { - height: Units::Pixels(thickness).into(), - width: Units::Stretch(1.0).into(), - ..Default::default() - }); - thumb_style.apply(Style { - height: Units::Pixels(thickness).into(), - width: Units::Percentage(thumb_size_percent * 100.0).into(), - top: Units::Pixels(0.0).into(), - left: Units::Percentage(-thumb_offset).into(), - ..Default::default() - }); - } else { - track_style.apply(Style { - width: Units::Pixels(thickness).into(), - height: Units::Stretch(1.0).into(), - ..Default::default() - }); - thumb_style.apply(Style { - width: Units::Pixels(thickness).into(), - height: Units::Percentage(thumb_size_percent * 100.0).into(), - top: Units::Percentage(-thumb_offset).into(), - left: Units::Pixels(0.0).into(), - ..Default::default() - }); - } + let mut track_style = + KStyle::default() + .with_style(&track_styles) + .with_style(KStyle { + background_color: track_color.into(), + border_radius: Corner::all(thickness / 2.0).into(), + ..Default::default() + }); - // === States === // - // A state determining whether we are currently dragging the thumb - let (is_dragging, set_is_dragging, ..) = use_state!(false); - // A state containing the UI coordinates of the initial click. - // This is used to get the difference from the current cursor coordinates, so that - // we can calculate how much the thumb should move. - let (start_pos, set_start_pos, ..) = use_state!((0.0, 0.0)); - // A state containing the scroll offsets when initially clicked. - // This is used in conjunction with `start_pos` to calculate the actual scrolled amount. - let (start_offset, set_start_offset, ..) = use_state!((0.0, 0.0)); + let mut border_color = thumb_color; + match &mut border_color { + Color::Rgba { + red, + green, + blue, + alpha, + } => { + *alpha = (*alpha - 0.2).max(0.0); + *red = (*red + 0.1).min(1.0); + *green = (*green + 0.1).min(1.0); + *blue = (*blue + 0.1).min(1.0); + } + _ => {} + } - // === Events === // - let on_track_event = OnEvent::new(move |ctx, event| match event.event_type { - EventType::MouseDown(data) => { - // --- Capture Cursor --- // - ctx.capture_cursor(event.current_target); - set_start_pos((data.position.0, data.position.1)); - set_is_dragging(true); + let mut thumb_style = KStyle::default() + .with_style(KStyle { + position_type: PositionType::SelfDirected.into(), + ..Default::default() + }) + .with_style(&thumb_styles) + .with_style(KStyle { + background_color: thumb_color.into(), + border_radius: Corner::all(thickness / 2.0).into(), + border: Edge::all(1.0).into(), + border_color: border_color.into(), + ..Default::default() + }); - // --- Calculate Start Offsets --- // - // Steps: - // 1. Get position relative to this widget - // 2. Convert relative pos to percentage [0-1] - // 3. Multiply by desired scrollable dimension - // 4. Map value to range padded by half thumb_size (both sides) - // 5. Update scroll - let mut old = scroll_ctx.get(); - let offset: (f32, f32) = if horizontal { - // 1. - let mut x = data.position.0 - layout.posx; - // 2. - x /= layout.width; - // 3. - x *= -scrollable_width; - // 4. - x = map_range( - x, - (-scrollable_width, 0.0), - (-scrollable_width - thumb_extents, thumb_extents), - ); - // 5. - old.set_scroll_x(x); + if scrollbar.horizontal { + track_style.apply(KStyle { + height: Units::Pixels(thickness).into(), + width: Units::Stretch(1.0).into(), + ..Default::default() + }); + thumb_style.apply(KStyle { + height: Units::Pixels(thickness).into(), + width: Units::Percentage(thumb_size_percent * 100.0).into(), + top: Units::Pixels(0.0).into(), + left: Units::Percentage(-thumb_offset).into(), + ..Default::default() + }); + } else { + track_style.apply(KStyle { + width: Units::Pixels(thickness).into(), + height: Units::Stretch(1.0).into(), + ..Default::default() + }); + thumb_style.apply(KStyle { + width: Units::Pixels(thickness).into(), + height: Units::Percentage(thumb_size_percent * 100.0).into(), + top: Units::Percentage(-thumb_offset).into(), + left: Units::Pixels(0.0).into(), + ..Default::default() + }); + } - (x, scroll_y) - } else { - // 1. - let mut y = data.position.1 - layout.posy; - // 2. - y /= layout.height; - // 3. - y *= -scrollable_height; - // 4. - y = map_range( - y, - (-scrollable_height, 0.0), - (-scrollable_height - thumb_extents, thumb_extents), - ); - // 5. - old.set_scroll_y(y); + // === Events === // + let on_event = OnEvent::new( + move |In((mut event_dispatcher_context, mut event, _entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut query: Query<&mut ScrollContext>| { + if let Ok(mut scroll_context) = query.get_mut(context_entity) { + match event.event_type { + EventType::MouseDown(data) => { + // --- Capture Cursor --- // + event_dispatcher_context + .capture_cursor(event.current_target); + scroll_context.start_pos = data.position.into(); + scroll_context.is_dragging = true; - (scroll_x, y) - }; + // --- Calculate Start Offsets --- // + // Steps: + // 1. Get position relative to this widget + // 2. Convert relative pos to percentage [0-1] + // 3. Multiply by desired scrollable dimension + // 4. Map value to range padded by half thumb_size (both sides) + // 5. Update scroll + let offset: (f32, f32) = if horizontal { + // 1. + let mut x = data.position.0 - layout.posx; + // 2. + x /= layout.width; + // 3. + x *= -scrollable_width; + // 4. + x = map_range( + x, + (-scrollable_width, 0.0), + (-scrollable_width - thumb_extents, thumb_extents), + ); + // 5. + scroll_context.set_scroll_x(x); - scroll_ctx.set(old); - set_start_offset(offset) - } - EventType::MouseUp(..) => { - // --- Release Cursor --- // - ctx.release_cursor(event.current_target); - set_is_dragging(false); - } - EventType::Hover(data) if is_dragging => { - // --- Move Thumb --- // - // Positional difference (scaled by thumb size) - let pos_diff = ( - (start_pos.0 - data.position.0) / thumb_size_percent, - (start_pos.1 - data.position.1) / thumb_size_percent, - ); - let mut old = scroll_ctx.get(); - if horizontal { - old.set_scroll_x(start_offset.0 + pos_diff.0); - } else { - old.set_scroll_y(start_offset.1 + pos_diff.1); - } - scroll_ctx.set(old); - } - EventType::Scroll(..) if is_dragging => { - // Prevent scrolling while dragging - // This is a bit of a hack to prevent issues when scrolling while dragging - event.stop_propagation(); - } - _ => {} - }); + (x, scroll_y) + } else { + // 1. + let mut y = data.position.1 - layout.posy; + // 2. + y /= layout.height; + // 3. + y *= -scrollable_height; + // 4. + y = map_range( + y, + (-scrollable_height, 0.0), + (-scrollable_height - thumb_extents, thumb_extents), + ); + // 5. + scroll_context.set_scroll_y(y); - let on_track_event = if disabled { None } else { Some(on_track_event) }; + (scroll_x, y) + }; + scroll_context.start_offset = offset.into(); + } + EventType::MouseUp(..) => { + // --- Release Cursor --- // + event_dispatcher_context + .release_cursor(event.current_target); + scroll_context.is_dragging = false; + } + EventType::Hover(data) => { + if scroll_context.is_dragging { + // --- Move Thumb --- // + // Positional difference (scaled by thumb size) + let pos_diff = ( + (scroll_context.start_pos.x - data.position.0) + / thumb_size_percent, + (scroll_context.start_pos.y - data.position.1) + / thumb_size_percent, + ); + let start_offset = scroll_context.start_offset; + if horizontal { + scroll_context + .set_scroll_x(start_offset.x + pos_diff.0); + } else { + scroll_context + .set_scroll_y(start_offset.y + pos_diff.1); + } + } + } + EventType::Scroll(..) if scroll_context.is_dragging => { + // Prevent scrolling while dragging + // This is a bit of a hack to prevent issues when scrolling while dragging + event.stop_propagation(); + } + _ => {} + } + } - // === Render === // - rsx! { - <Background on_event={on_track_event} styles={Some(track_style)}> - <Clip> - <Background styles={Some(thumb_style)} /> - </Clip> - </Background> + (event_dispatcher_context, event) + }, + ); + + let parent_id = Some(entity); + rsx! { + <BackgroundBundle on_event={on_event} styles={track_style}> + <ClipBundle> + <BackgroundBundle styles={thumb_style} /> + </ClipBundle> + </BackgroundBundle> + } + + return true; + } + } + } } + false } diff --git a/src/widgets/scroll/scroll_box.rs b/src/widgets/scroll/scroll_box.rs index 67aa93f..9790e7a 100644 --- a/src/widgets/scroll/scroll_box.rs +++ b/src/widgets/scroll/scroll_box.rs @@ -1,19 +1,31 @@ -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{PositionType, Style, Units}, - widget, Bound, Children, EventType, MutableBound, OnEvent, ScrollUnit, WidgetProps, +use bevy::prelude::{ + Bundle, Changed, Color, Commands, Component, Entity, In, Or, ParamSet, Query, With, }; -use kayak_core::styles::LayoutType; -use kayak_core::{Color, GeometryChanged, OnLayout}; - -use crate::widgets::{Clip, Element, If}; +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + cursor::ScrollUnit, + event::{Event, EventType}, + event_dispatcher::EventDispatcherContext, + layout::{GeometryChanged, LayoutEvent}, + on_event::OnEvent, + on_layout::OnLayout, + prelude::{constructor, rsx, WidgetContext}, + styles::{KStyle, LayoutType, PositionType, RenderCommand, Units}, + widget::Widget, + widgets::{ + scroll::{ + scroll_bar::{ScrollBarBundle, ScrollBarProps}, + scroll_content::ScrollContentBundle, + }, + ClipBundle, ElementBundle, + }, +}; -use super::{ScrollBar, ScrollContent, ScrollContext, ScrollMode}; +use super::scroll_context::ScrollContext; -/// Props used by the [`ScrollBox`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] +#[derive(Component, Default)] pub struct ScrollBoxProps { /// If true, always shows scrollbars even when there's nothing to scroll /// @@ -24,8 +36,6 @@ pub struct ScrollBoxProps { pub disable_horizontal: bool, /// If true, disables vertical scrolling pub disable_vertical: bool, - /// The scroll mode to use - pub mode: ScrollMode, /// If true, hides the horizontal scrollbar pub hide_horizontal: bool, /// If true, hides the vertical scrollbar @@ -37,189 +47,231 @@ pub struct ScrollBoxProps { /// The color of the scrollbar thumb pub thumb_color: Option<Color>, /// The styles of the scrollbar thumb - pub thumb_styles: Option<Style>, + pub thumb_styles: Option<KStyle>, /// The color of the scrollbar track pub track_color: Option<Color>, /// The styles of the scrollbar track - pub track_styles: Option<Style>, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnLayout)] - on_layout: Option<OnLayout>, + pub track_styles: Option<KStyle>, +} + +impl Widget for ScrollBoxProps {} + +#[derive(Bundle)] +pub struct ScrollBoxBundle { + pub scroll_box_props: ScrollBoxProps, + pub styles: KStyle, + pub children: KChildren, + pub on_layout: OnLayout, + pub widget_name: WidgetName, } -#[widget] -/// A widget that creates a scrollable area for overflowing content -/// -/// # Props -/// -/// __Type:__ [`ScrollBoxProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ⌠| -/// | `focusable` | ⌠| -/// -pub fn ScrollBox(props: ScrollBoxProps) { - // === Configuration === // - let always_show_scrollbar = props.always_show_scrollbar; - let disable_horizontal = props.disable_horizontal; - let disable_vertical = props.disable_vertical; - let hide_horizontal = props.hide_horizontal; - let hide_vertical = props.hide_vertical; - let mode = props.mode; - let scrollbar_thickness = props.scrollbar_thickness.unwrap_or(10.0); - let scroll_line = props.scroll_line.unwrap_or(16.0); - let thumb_color = props.thumb_color; - let thumb_styles = props.thumb_styles.clone(); - let track_color = props.track_color; - let track_styles = props.track_styles.clone(); - - // === Scroll === // - let scroll_ctx = context.create_provider(ScrollContext { - mode, - ..Default::default() - }); - let scroll: ScrollContext = scroll_ctx.get(); - let scroll_x = scroll.scroll_x(); - let scroll_y = scroll.scroll_y(); - let scrollable_width = scroll.scrollable_width(); - let scrollable_height = scroll.scrollable_height(); - - let hori_thickness = scrollbar_thickness; - let vert_thickness = scrollbar_thickness; - - let hide_horizontal = - hide_horizontal || !always_show_scrollbar && scrollable_width < f32::EPSILON; - let hide_vertical = hide_vertical || !always_show_scrollbar && scrollable_height < f32::EPSILON; - - { - let mut next = scroll_ctx.get(); - next.pad_x = if hide_vertical { 0.0 } else { vert_thickness }; - next.pad_y = if hide_horizontal { 0.0 } else { hori_thickness }; - - if next.pad_x != scroll.pad_x || next.pad_y != scroll.pad_y { - scroll_ctx.set(next); +impl Default for ScrollBoxBundle { + fn default() -> Self { + Self { + scroll_box_props: Default::default(), + styles: Default::default(), + children: Default::default(), + on_layout: Default::default(), + widget_name: ScrollBoxProps::default().get_name(), } } +} - // === Layout === // - let _scroll_ctx = scroll_ctx.clone(); - props.on_layout = Some(OnLayout::new(move |_, evt| { - if evt - .flags - .intersects(GeometryChanged::WIDTH_CHANGED | GeometryChanged::HEIGHT_CHANGED) +pub fn update_scroll_box( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: ParamSet<( + Query<Entity, Or<(Changed<ScrollBoxProps>, Changed<KChildren>, With<Mounted>)>>, + Query<(&ScrollBoxProps, &mut KStyle, &KChildren, &mut OnLayout)>, + )>, + mut context_query: ParamSet<( + Query<Entity, Changed<ScrollContext>>, + Query<&ScrollContext>, + Query<&mut ScrollContext>, + )>, +) -> bool { + if !context_query.p0().is_empty() || !query.p0().is_empty() { + if let Ok((scroll_box, mut styles, scroll_box_children, mut on_layout)) = + query.p1().get_mut(entity) { - let mut next = _scroll_ctx.get(); - next.scrollbox_width = evt.layout.width; - next.scrollbox_height = evt.layout.height; - _scroll_ctx.set(next); - } - })); - - // === Styles === // - props.styles = Some( - Style::default() - .with_style(Style { - render_command: RenderCommand::Layout.into(), - ..Default::default() - }) - .with_style(&props.styles) - .with_style(Style { - width: Units::Stretch(1.0).into(), - height: Units::Stretch(1.0).into(), - ..Default::default() - }), - ); - - let hbox_styles = Style::default().with_style(Style { - render_command: RenderCommand::Layout.into(), - layout_type: LayoutType::Row.into(), - width: Units::Stretch(1.0).into(), - ..Default::default() - }); - let vbox_styles = Style::default().with_style(Style { - render_command: RenderCommand::Layout.into(), - layout_type: LayoutType::Column.into(), - width: Units::Stretch(1.0).into(), - ..Default::default() - }); - - let content_styles = Style::default().with_style(Style { - position_type: PositionType::SelfDirected.into(), - top: Units::Pixels(scroll_y).into(), - left: Units::Pixels(scroll_x).into(), - ..Default::default() - }); - - // === Events === // - let event_handler = OnEvent::new(move |_, event| match event.event_type { - EventType::Scroll(evt) => { - match evt.delta { - ScrollUnit::Line { x, y } => { - let mut old = scroll_ctx.get(); - if !disable_horizontal { - old.set_scroll_x(scroll_x - x * scroll_line); - } - if !disable_vertical { - old.set_scroll_y(scroll_y + y * scroll_line); - } - scroll_ctx.set(old); - } - ScrollUnit::Pixel { x, y } => { - let mut old = scroll_ctx.get(); - if !disable_horizontal { - old.set_scroll_x(scroll_x - x); + if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) + { + if let Ok(scroll_context) = context_query.p1().get(context_entity).cloned() { + // === Configuration === // + let always_show_scrollbar = scroll_box.always_show_scrollbar; + let disable_horizontal = scroll_box.disable_horizontal; + let disable_vertical = scroll_box.disable_vertical; + let hide_horizontal = scroll_box.hide_horizontal; + let hide_vertical = scroll_box.hide_vertical; + let scrollbar_thickness = scroll_box.scrollbar_thickness.unwrap_or(10.0); + let scroll_line = scroll_box.scroll_line.unwrap_or(16.0); + let thumb_color = scroll_box.thumb_color; + let thumb_styles = scroll_box.thumb_styles.clone(); + let track_color = scroll_box.track_color; + let track_styles = scroll_box.track_styles.clone(); + + let scroll_x = scroll_context.scroll_x(); + let scroll_y = scroll_context.scroll_y(); + let scrollable_width = scroll_context.scrollable_width(); + let scrollable_height = scroll_context.scrollable_height(); + + let hori_thickness = scrollbar_thickness; + let vert_thickness = scrollbar_thickness; + + let hide_horizontal = hide_horizontal + || !always_show_scrollbar && scrollable_width < f32::EPSILON; + let hide_vertical = + hide_vertical || !always_show_scrollbar && scrollable_height < f32::EPSILON; + + let pad_x = if hide_vertical { 0.0 } else { vert_thickness }; + let pad_y = if hide_horizontal { 0.0 } else { hori_thickness }; + + if pad_x != scroll_context.pad_x || pad_y != scroll_context.pad_y { + if let Ok(mut scroll_context_mut) = + context_query.p2().get_mut(context_entity) + { + scroll_context_mut.pad_x = pad_x; + scroll_context_mut.pad_y = pad_y; + } } - if !disable_vertical { - old.set_scroll_y(scroll_y + y); + + *on_layout = OnLayout::new( + move |In((event, _entity)): In<(LayoutEvent, Entity)>, + mut query: Query<&mut ScrollContext>| { + if event.flags.intersects( + GeometryChanged::WIDTH_CHANGED | GeometryChanged::HEIGHT_CHANGED, + ) { + if let Ok(mut scroll) = query.get_mut(context_entity) { + scroll.scrollbox_width = event.layout.width; + scroll.scrollbox_height = event.layout.height; + } + } + + event + }, + ); + + // === Styles === // + *styles = KStyle::default() + .with_style(KStyle { + render_command: RenderCommand::Layout.into(), + ..Default::default() + }) + .with_style(styles.clone()) + .with_style(KStyle { + width: Units::Stretch(1.0).into(), + height: Units::Stretch(1.0).into(), + ..Default::default() + }); + + let hbox_styles = KStyle::default().with_style(KStyle { + render_command: RenderCommand::Layout.into(), + layout_type: LayoutType::Row.into(), + width: Units::Stretch(1.0).into(), + ..Default::default() + }); + let vbox_styles = KStyle::default().with_style(KStyle { + render_command: RenderCommand::Layout.into(), + layout_type: LayoutType::Column.into(), + width: Units::Stretch(1.0).into(), + ..Default::default() + }); + + let content_styles = KStyle::default().with_style(KStyle { + position_type: PositionType::SelfDirected.into(), + top: Units::Pixels(scroll_y).into(), + left: Units::Pixels(scroll_x).into(), + ..Default::default() + }); + + let event_handler = OnEvent::new( + move |In((event_dispatcher_context, mut event, _entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut query: Query<&mut ScrollContext>| { + if let Ok(mut scroll_context) = query.get_mut(context_entity) { + match event.event_type { + EventType::Scroll(evt) => { + match evt.delta { + ScrollUnit::Line { x, y } => { + if !disable_horizontal { + scroll_context + .set_scroll_x(scroll_x - x * scroll_line); + } + if !disable_vertical { + scroll_context + .set_scroll_y(scroll_y + y * scroll_line); + } + } + ScrollUnit::Pixel { x, y } => { + if !disable_horizontal { + scroll_context.set_scroll_x(scroll_x - x); + } + if !disable_vertical { + scroll_context.set_scroll_y(scroll_y + y); + } + } + } + event.stop_propagation(); + } + _ => {} + } + } + (event_dispatcher_context, event) + }, + ); + + let parent_id = Some(entity); + rsx! { + <ElementBundle on_event={event_handler} styles={hbox_styles}> + <ElementBundle styles={vbox_styles}> + <ClipBundle> + <ScrollContentBundle + children={scroll_box_children.clone()} + styles={content_styles} + /> + </ClipBundle> + {if !hide_horizontal { + constructor! { + <ScrollBarBundle + scrollbar_props={ScrollBarProps { + disabled: disable_horizontal, + horizontal: true, + thickness: hori_thickness, + thumb_color: thumb_color, + thumb_styles: thumb_styles.clone(), + track_color: track_color, + track_styles: track_styles.clone(), + ..Default::default() + }} + /> + } + }} + </ElementBundle> + {if !hide_vertical { + constructor! { + <ScrollBarBundle + scrollbar_props={ScrollBarProps { + disabled: disable_vertical, + thickness: hori_thickness, + thumb_color: thumb_color.clone(), + thumb_styles: thumb_styles.clone(), + track_color: track_color, + track_styles: track_styles.clone(), + ..Default::default() + }} + /> + } + }} + </ElementBundle> } - scroll_ctx.set(old); + + return true; } } - event.stop_propagation(); } - _ => {} - }); - - // === Render === // - let children = props.get_children(); - rsx! { - <Element on_event={Some(event_handler)}> - <Element styles={Some(hbox_styles)}> - <Element styles={Some(vbox_styles)}> - <Clip> - <ScrollContent styles={Some(content_styles)}> - {children} - </ScrollContent> - </Clip> - <If condition={!hide_horizontal}> - <ScrollBar - disabled={disable_horizontal} - horizontal={true} - thickness={hori_thickness} - thumb_color={thumb_color} - thumb_styles={thumb_styles} - track_color={track_color} - track_styles={track_styles} - /> - </If> - </Element> - <If condition={!hide_vertical}> - <ScrollBar - disabled={disable_vertical} - thickness={hori_thickness} - thumb_color={thumb_color} - thumb_styles={thumb_styles} - track_color={track_color} - track_styles={track_styles} - /> - </If> - </Element> - </Element> } + false } diff --git a/src/widgets/scroll/scroll_content.rs b/src/widgets/scroll/scroll_content.rs index 92c9863..d59e703 100644 --- a/src/widgets/scroll/scroll_content.rs +++ b/src/widgets/scroll/scroll_content.rs @@ -1,69 +1,106 @@ -use crate::core::{rsx, styles::Style, widget, Bound, Children, MutableBound, WidgetProps}; -use kayak_core::render_command::RenderCommand; -use kayak_core::styles::{LayoutType, Units}; -use kayak_core::{GeometryChanged, OnLayout}; +use bevy::prelude::{Bundle, Changed, Component, Entity, In, Or, ParamSet, Query, With}; -use super::ScrollContext; +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + layout::GeometryChanged, + layout::LayoutEvent, + on_layout::OnLayout, + prelude::WidgetContext, + styles::{KStyle, LayoutType, RenderCommand, Units}, + widget::Widget, +}; -/// Props used by the [`ScrollContent`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub(super) struct ScrollContentProps { - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, -} +use super::scroll_context::ScrollContext; + +#[derive(Component, Default)] +pub struct ScrollContentProps; -#[widget] -/// A widget that contains the content of a [`ScrollBox`](crate::ScrollBox) widget -/// -/// The main purpose of this widget is to calculate the size of its children on render. This -/// is needed by the [`ScrollContext`] in order to function properly. -pub(super) fn ScrollContent(props: ScrollContentProps) { - // === Scroll === // - let scroll_ctx = context.create_consumer::<ScrollContext>().unwrap(); - let ScrollContext { - scrollbox_width, - scrollbox_height, - pad_x, - pad_y, - .. - } = scroll_ctx.get(); +impl Widget for ScrollContentProps {} - // === Layout === // - props.on_layout = Some(OnLayout::new(move |_, evt| { - if evt - .flags - .intersects(GeometryChanged::WIDTH_CHANGED | GeometryChanged::HEIGHT_CHANGED) - { - let mut scroll: ScrollContext = scroll_ctx.get(); - scroll.content_width = evt.layout.width; - scroll.content_height = evt.layout.height; - scroll_ctx.set(scroll); +#[derive(Bundle)] +pub struct ScrollContentBundle { + pub scroll_content_props: ScrollContentProps, + pub styles: KStyle, + pub children: KChildren, + pub on_layout: OnLayout, + pub widget_name: WidgetName, +} + +impl Default for ScrollContentBundle { + fn default() -> Self { + Self { + scroll_content_props: Default::default(), + styles: Default::default(), + children: Default::default(), + on_layout: Default::default(), + widget_name: ScrollContentProps::default().get_name(), } - })); + } +} + +pub fn update_scroll_content( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut query: ParamSet<( + Query< + Entity, + Or<( + Changed<ScrollContentProps>, + Changed<KChildren>, + With<Mounted>, + )>, + >, + Query<(&mut KStyle, &KChildren, &mut OnLayout), With<ScrollContentProps>>, + )>, + mut context_query: ParamSet<(Query<Entity, Changed<ScrollContext>>, Query<&ScrollContext>)>, +) -> bool { + if !context_query.p0().is_empty() || !query.p0().is_empty() { + if let Ok((mut styles, children, mut on_layout)) = query.p1().get_mut(entity) { + if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) + { + if let Ok(scroll_context) = context_query.p1().get(context_entity) { + // === OnLayout === // + *on_layout = OnLayout::new( + move |In((event, _entity)): In<(LayoutEvent, Entity)>, + mut query: Query<&mut ScrollContext>| { + if event.flags.intersects( + GeometryChanged::WIDTH_CHANGED | GeometryChanged::HEIGHT_CHANGED, + ) { + if let Ok(mut scroll) = query.get_mut(context_entity) { + scroll.content_width = event.layout.width; + scroll.content_height = event.layout.height; + } + } + + event + }, + ); - // === Styles === // - props.styles = Some( - Style::default() - .with_style(Style { - render_command: RenderCommand::Layout.into(), - layout_type: LayoutType::Column.into(), - min_width: Units::Pixels(scrollbox_width - pad_x).into(), - min_height: Units::Stretch(scrollbox_height - pad_y).into(), - width: Units::Auto.into(), - height: Units::Auto.into(), - ..Default::default() - }) - .with_style(&props.styles), - ); + // === Styles === // + *styles = KStyle::default() + .with_style(KStyle { + render_command: RenderCommand::Layout.into(), + layout_type: LayoutType::Column.into(), + min_width: Units::Pixels( + scroll_context.scrollbox_width - scroll_context.pad_x, + ) + .into(), + min_height: Units::Stretch( + scroll_context.scrollbox_height - scroll_context.pad_y, + ) + .into(), + width: Units::Auto.into(), + height: Units::Auto.into(), + ..Default::default() + }) + .with_style(styles.clone()); - // === Render === // - rsx! { - <> - {children} - </> + children.process(&widget_context, Some(entity)); + + return true; + } + } + } } + false } diff --git a/src/widgets/scroll/scroll_context.rs b/src/widgets/scroll/scroll_context.rs index 2b25c76..d73b4cd 100644 --- a/src/widgets/scroll/scroll_context.rs +++ b/src/widgets/scroll/scroll_context.rs @@ -1,5 +1,15 @@ +use bevy::prelude::{Bundle, Changed, Commands, Component, Entity, In, Or, Query, Vec2, With}; + +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::KStyle, + widget::Widget, +}; + /// Context data provided by a [`ScrollBox`](crate::ScrollBox) widget -#[derive(Default, Debug, Copy, Clone, PartialEq)] +#[derive(Component, Default, Debug, Copy, Clone, PartialEq)] pub struct ScrollContext { pub(super) scroll_x: f32, pub(super) scroll_y: f32, @@ -10,6 +20,9 @@ pub struct ScrollContext { pub(super) pad_x: f32, pub(super) pad_y: f32, pub(super) mode: ScrollMode, + pub(super) is_dragging: bool, + pub(super) start_pos: Vec2, + pub(super) start_offset: Vec2, } #[non_exhaustive] @@ -120,3 +133,51 @@ impl ScrollContext { value.clamp(min, max) } } + +#[derive(Component, Default)] +pub struct ScrollContextProvider { + initial_value: ScrollContext, +} + +impl Widget for ScrollContextProvider {} + +#[derive(Bundle)] +pub struct ScrollContextProviderBundle { + pub scroll_context_provider: ScrollContextProvider, + pub children: KChildren, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for ScrollContextProviderBundle { + fn default() -> Self { + Self { + scroll_context_provider: Default::default(), + children: KChildren::default(), + styles: Default::default(), + widget_name: ScrollContextProvider::default().get_name(), + } + } +} + +pub fn update_scroll_context( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: Query< + (&ScrollContextProvider, &KChildren), + Or<( + Changed<ScrollContextProvider>, + Changed<KChildren>, + With<Mounted>, + )>, + >, +) -> bool { + if let Ok((context_provider, children)) = query.get_mut(entity) { + let context_entity = commands.spawn(context_provider.initial_value).id(); + widget_context.set_context_entity::<ScrollContext>(Some(entity), context_entity); + children.process(&widget_context, Some(entity)); + return true; + } + + false +} diff --git a/src/widgets/spin_box.rs b/src/widgets/spin_box.rs deleted file mode 100644 index c60dba5..0000000 --- a/src/widgets/spin_box.rs +++ /dev/null @@ -1,352 +0,0 @@ -use std::fmt::Debug; - -use crate::{ - core::{ - render_command::RenderCommand, - rsx, - styles::{Corner, Style, Units}, - widget, Bound, Children, Color, EventType, MutableBound, OnEvent, WidgetProps, - }, - widgets::{Button, ChangeEvent}, -}; -use kayak_core::{ - styles::{LayoutType, StyleProp}, - CursorIcon, OnLayout, -}; -use kayak_render_macros::use_state; - -use crate::widgets::{Background, Clip, OnChange, Text}; - -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum SpinBoxStyle { - Horizontal, - Vertical, -} - -impl Default for SpinBoxStyle { - fn default() -> Self { - SpinBoxStyle::Horizontal - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct SpinBoxProps { - /// If true, prevents the widget from being focused (and consequently edited) - pub disabled: bool, - /// A callback for when the text value was changed - pub on_change: Option<OnChange>, - /// The text to display when the user input is empty - pub placeholder: Option<String>, - /// Whether spinbox is horizontally or vertically aligned. - pub spin_button_style: SpinBoxStyle, - /// The user input - /// - /// This is a controlled state. You _must_ set this to the value to you wish to be displayed. - /// You can use the [`on_change`] callback to update this prop as the user types. - pub value: String, - pub styles: Option<Style>, - /// Text on increment button defaults to `>` - pub incr_str: String, - /// Text on decrement button defaults to `<` - pub decr_str: String, - /// Events on increment button press - pub on_incr_event: Option<OnEvent>, - /// Events on decrement button press - pub on_decr_event: Option<OnEvent>, - /// Events for text edit - pub on_event: Option<OnEvent>, - /// Minimal value - pub min_val: f32, - /// Maximal value - pub max_val: f32, - pub children: Option<Children>, - pub on_layout: Option<OnLayout>, - pub focusable: Option<bool>, -} - -impl SpinBoxProps { - pub fn get_float(&self) -> f32 { - self.value.parse::<f32>().unwrap_or_default() - } - - pub fn get_int(&self) -> i16 { - let temp_float = self.get_float(); - if temp_float > f32::from(i16::MAX) { - i16::MAX - } else if temp_float < f32::from(i16::MIN) { - i16::MIN - } else { - temp_float.round() as i16 - } - } -} - -impl Default for SpinBoxProps { - fn default() -> SpinBoxProps { - SpinBoxProps { - incr_str: "+".into(), - decr_str: "-".into(), - disabled: Default::default(), - on_change: Default::default(), - placeholder: Default::default(), - value: Default::default(), - styles: Default::default(), - spin_button_style: Default::default(), - children: Default::default(), - on_incr_event: Default::default(), - on_decr_event: Default::default(), - on_event: Default::default(), - min_val: f32::MIN, - max_val: f32::MAX, - on_layout: Default::default(), - focusable: Default::default(), - } - } -} - -impl WidgetProps for SpinBoxProps { - fn get_children(&self) -> Option<Children> { - self.children.clone() - } - - fn set_children(&mut self, children: Option<Children>) { - self.children = children; - } - - fn get_styles(&self) -> Option<Style> { - self.styles.clone() - } - - fn get_on_event(&self) -> Option<OnEvent> { - self.on_event.clone() - } - - fn get_on_layout(&self) -> Option<OnLayout> { - self.on_layout.clone() - } - - fn get_focusable(&self) -> Option<bool> { - Some(!self.disabled) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct FocusSpinbox(pub bool); - -#[widget] -/// A widget that displays a spinnable text field -/// -/// # Props -/// -/// __Type:__ [`SpinBoxProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn SpinBox(props: SpinBoxProps) { - let SpinBoxProps { - on_change, - placeholder, - value, - max_val, - min_val, - spin_button_style, - .. - } = props.clone(); - - props.styles = Some( - Style::default() - // Required styles - .with_style(Style { - render_command: RenderCommand::Layout.into(), - ..Default::default() - }) - // Apply any prop-given styles - .with_style(&props.styles) - // If not set by props, apply these styles - .with_style(Style { - top: Units::Pixels(0.0).into(), - bottom: Units::Pixels(0.0).into(), - height: Units::Pixels(26.0).into(), - cursor: CursorIcon::Text.into(), - ..Default::default() - }), - ); - - let background_styles = Style { - background_color: Color::new(0.176, 0.196, 0.215, 1.0).into(), - border_radius: Corner::all(5.0).into(), - height: Units::Pixels(26.0).into(), - padding_left: Units::Pixels(5.0).into(), - padding_right: Units::Pixels(5.0).into(), - ..Default::default() - }; - - let has_focus = context.create_state(FocusSpinbox(false)).unwrap(); - - let mut current_value = value.clone(); - let cloned_on_change = on_change.clone(); - let cloned_has_focus = has_focus.clone(); - - props.on_event = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::CharInput { c } => { - if !cloned_has_focus.get().0 { - return; - } - if is_backspace(c) { - if !current_value.is_empty() { - current_value.truncate(current_value.len() - 1); - } - } else if !c.is_control() { - current_value.push(c); - } - if let Some(on_change) = cloned_on_change.as_ref() { - if let Ok(mut on_change) = on_change.0.write() { - on_change(ChangeEvent { - value: current_value.clone(), - }); - } - } - } - EventType::Focus => cloned_has_focus.set(FocusSpinbox(true)), - EventType::Blur => cloned_has_focus.set(FocusSpinbox(false)), - _ => {} - })); - - let text_styles = if value.is_empty() || (has_focus.get().0 && value.is_empty()) { - Style { - color: Color::new(0.5, 0.5, 0.5, 1.0).into(), - ..Style::default() - } - } else { - Style { - width: Units::Stretch(100.0).into(), - ..Style::default() - } - }; - - let button_style = match spin_button_style { - SpinBoxStyle::Horizontal => Some(Style { - height: Units::Pixels(24.0).into(), - width: Units::Pixels(24.0).into(), - ..Default::default() - }), - SpinBoxStyle::Vertical => Some(Style { - height: Units::Pixels(12.0).into(), - width: Units::Pixels(24.0).into(), - - ..Default::default() - }), - }; - - let value = if value.is_empty() { - placeholder.unwrap_or_else(|| value.clone()) - } else { - value - }; - - let row = Style { - layout_type: StyleProp::Value(LayoutType::Row), - ..Style::default() - }; - - let col = Style { - layout_type: StyleProp::Value(LayoutType::Column), - height: Units::Stretch(100.0).into(), - width: Units::Pixels(26.0).into(), - ..Style::default() - }; - - let incr_str = props.clone().incr_str; - let decr_str = props.clone().decr_str; - - let (spin_value, set_val, _) = use_state!(value); - let x = spin_value.parse::<f32>().unwrap_or_default(); - let decr_fn = set_val.clone(); - let incr_fn = set_val.clone(); - - let incr_event = if let Some(event) = props.clone().on_incr_event { - event - } else { - OnEvent::new(move |_, event| match event.event_type { - EventType::Click(_) => { - if x >= max_val { - return; - } - incr_fn((x + 1.0f32).to_string()); - } - _ => {} - }) - }; - - let decr_event = if let Some(event) = props.clone().on_decr_event { - event - } else { - OnEvent::new(move |_, event| match event.event_type { - EventType::Click(_) => { - if x <= min_val { - return; - } - decr_fn((x - 1.0f32).to_string()); - } - _ => {} - }) - }; - - match spin_button_style { - SpinBoxStyle::Horizontal => { - rsx! { - <Background styles={Some(background_styles)}> - <Clip styles={Some(row)}> - <Button styles={button_style} on_event={Some(decr_event)}> - <Text content={decr_str} /> - </Button> - <Text - content={spin_value} - size={14.0} - styles={Some(text_styles)} - /> - <Button styles={button_style} on_event={Some(incr_event)}> - <Text content={incr_str} /> - </Button> - </Clip> - </Background> - } - } - SpinBoxStyle::Vertical => { - rsx! { - <Background styles={Some(background_styles)}> - - <Clip styles={Some(row)}> - <Text - content={spin_value} - size={14.0} - styles={Some(text_styles)} - /> - <Clip styles={Some(col)}> - - <Button styles={button_style} on_event={Some(incr_event)}> - <Text content={incr_str} size={11.0} /> - </Button> - <Button styles={button_style} on_event={Some(decr_event)}> - <Text content={decr_str} size={11.0}/> - </Button> - </Clip> - </Clip> - </Background> - } - } - } -} - -/// Checks if the given character contains the "Backspace" sequence -/// -/// Context: [Wikipedia](https://en.wikipedia.org/wiki/Backspace#Common_use) -fn is_backspace(c: char) -> bool { - c == '\u{8}' || c == '\u{7f}' -} diff --git a/src/widgets/text.rs b/src/widgets/text.rs index ad4269d..b6e4d01 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -1,11 +1,14 @@ -use crate::core::{ - render_command::RenderCommand, - styles::{Style, StyleProp}, - widget, CursorIcon, OnEvent, OnLayout, WidgetProps, +use bevy::prelude::*; +use kayak_font::Alignment; + +use crate::{ + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -/// Props used by the [`Text`] widget -#[derive(WidgetProps, Debug, PartialEq, Clone)] +#[derive(Component)] pub struct TextProps { /// The string to display pub content: String, @@ -23,14 +26,8 @@ pub struct TextProps { /// /// Negative values have no effect pub size: f32, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, + /// Text alignment. + pub alignment: Alignment, } impl Default for TextProps { @@ -41,49 +38,57 @@ impl Default for TextProps { line_height: None, show_cursor: false, size: -1.0, - styles: None, - on_event: None, - on_layout: None, - focusable: None, + alignment: Alignment::Start, } } } -#[widget] -/// A widget that renders plain text -/// -/// # Props -/// -/// __Type:__ [`TextProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ⌠| -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Text(props: TextProps) { - let mut styles = Style { - render_command: StyleProp::Value(RenderCommand::Text { - content: props.content.clone(), - }), - ..Default::default() - }; +impl Widget for TextProps {} - if let Some(ref font) = props.font { - styles.font = StyleProp::Value(font.clone()); - } - if props.show_cursor { - styles.cursor = StyleProp::Value(CursorIcon::Text); - } - if props.size >= 0.0 { - styles.font_size = StyleProp::Value(props.size); +#[derive(Bundle)] +pub struct TextWidgetBundle { + pub text: TextProps, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for TextWidgetBundle { + fn default() -> Self { + Self { + text: Default::default(), + styles: Default::default(), + widget_name: TextProps::default().get_name(), + } } - if let Some(line_height) = props.line_height { - styles.line_height = StyleProp::Value(line_height); +} + +pub fn text_update( + In((_, entity)): In<(WidgetContext, Entity)>, + mut query: Query<(&mut KStyle, &TextProps), Or<(Changed<TextProps>, With<Mounted>)>>, +) -> bool { + if let Ok((mut style, text)) = query.get_mut(entity) { + style.render_command = StyleProp::Value(RenderCommand::Text { + content: text.content.clone(), + alignment: text.alignment, + }); + + if let Some(ref font) = text.font { + style.font = StyleProp::Value(font.clone()); + } + // if text.show_cursor { + // style.cursor = StyleProp::Value(CursorIcon::Text); + // } + if text.size >= 0.0 { + style.font_size = StyleProp::Value(text.size); + } + if let Some(line_height) = text.line_height { + style.line_height = StyleProp::Value(line_height); + } + + // style.cursor = CursorIcon::Hand.into(); + + return true; } - props.styles = Some(styles.with_style(&props.styles)); + false } diff --git a/src/widgets/text_box.rs b/src/widgets/text_box.rs index e9cfc8a..f0d6708 100644 --- a/src/widgets/text_box.rs +++ b/src/widgets/text_box.rs @@ -1,23 +1,29 @@ +use bevy::prelude::{ + Bundle, Changed, Color, Commands, Component, Entity, In, Or, ParamSet, Query, With, +}; +use kayak_ui_macros::rsx; + use crate::{ - core::{ - render_command::RenderCommand, - rsx, - styles::{Corner, Style, Units}, - widget, Bound, Children, Color, EventType, MutableBound, OnEvent, WidgetProps, + context::{Mounted, WidgetName}, + event::{Event, EventType}, + event_dispatcher::EventDispatcherContext, + on_event::OnEvent, + on_layout::OnLayout, + prelude::{KChildren, OnChange, WidgetContext}, + styles::{Corner, KStyle, RenderCommand, StyleProp, Units}, + widget::Widget, + widgets::{ + text::{TextProps, TextWidgetBundle}, + BackgroundBundle, ClipBundle, }, - widgets::ChangeEvent, + Focusable, }; -use kayak_core::{CursorIcon, OnLayout}; - -use crate::widgets::{Background, Clip, OnChange, Text}; /// Props used by the [`TextBox`] widget -#[derive(Default, Debug, PartialEq, Clone)] +#[derive(Component, Default, Debug, PartialEq, Clone)] pub struct TextBoxProps { /// If true, prevents the widget from being focused (and consequently edited) pub disabled: bool, - /// A callback for when the text value was changed - pub on_change: Option<OnChange>, /// The text to display when the user input is empty pub placeholder: Option<String>, /// The user input @@ -25,43 +31,17 @@ pub struct TextBoxProps { /// This is a controlled state. You _must_ set this to the value to you wish to be displayed. /// You can use the [`on_change`] callback to update this prop as the user types. pub value: String, - pub styles: Option<Style>, - pub children: Option<Children>, - pub on_event: Option<OnEvent>, - pub on_layout: Option<OnLayout>, - pub focusable: Option<bool>, } -impl WidgetProps for TextBoxProps { - fn get_children(&self) -> Option<Children> { - self.children.clone() - } - - fn set_children(&mut self, children: Option<Children>) { - self.children = children; - } - - fn get_styles(&self) -> Option<Style> { - self.styles.clone() - } - - fn get_on_event(&self) -> Option<OnEvent> { - self.on_event.clone() - } - - fn get_on_layout(&self) -> Option<OnLayout> { - self.on_layout.clone() - } - - fn get_focusable(&self) -> Option<bool> { - Some(!self.disabled) - } +#[derive(Component, Default)] +pub struct TextBoxState { + pub focused: bool, } -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Focus(pub bool); +pub struct TextBoxValue(pub String); + +impl Widget for TextBoxProps {} -#[widget] /// A widget that displays a text input field /// /// # Props @@ -70,106 +50,158 @@ pub struct Focus(pub bool); /// /// | Common Prop | Accepted | /// | :---------: | :------: | -/// | `children` | ✅ | +/// | `children` | ⌠| /// | `styles` | ✅ | /// | `on_event` | ✅ | /// | `on_layout` | ✅ | -/// | `focusable` | ✅ | /// -pub fn TextBox(props: TextBoxProps) { - let TextBoxProps { - on_change, - placeholder, - value, - .. - } = props.clone(); - - props.styles = Some( - Style::default() - // Required styles - .with_style(Style { - render_command: RenderCommand::Layout.into(), - ..Default::default() - }) - // Apply any prop-given styles - .with_style(&props.styles) - // If not set by props, apply these styles - .with_style(Style { - top: Units::Pixels(0.0).into(), - bottom: Units::Pixels(0.0).into(), +#[derive(Bundle)] +pub struct TextBoxBundle { + pub text_box: TextBoxProps, + pub styles: KStyle, + pub on_event: OnEvent, + pub on_layout: OnLayout, + pub on_change: OnChange, + pub focusable: Focusable, + pub widget_name: WidgetName, +} + +impl Default for TextBoxBundle { + fn default() -> Self { + Self { + text_box: Default::default(), + styles: Default::default(), + on_event: Default::default(), + on_layout: Default::default(), + on_change: Default::default(), + focusable: Default::default(), + widget_name: TextBoxProps::default().get_name(), + } + } +} + +pub fn update_text_box( + In((widget_context, entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: ParamSet<( + Query<Entity, Or<(Changed<TextBoxProps>, Changed<KStyle>, With<Mounted>)>>, + Query<(&mut KStyle, &TextBoxProps, &mut OnEvent, &OnChange)>, + )>, + mut context_query: ParamSet<(Query<Entity, Changed<TextBoxState>>, Query<&TextBoxState>)>, +) -> bool { + if !query.p0().is_empty() || !context_query.p0().is_empty() { + if let Ok((mut styles, text_box, mut on_event, on_change)) = query.p1().get_mut(entity) { + let state_entity = widget_context.get_context_entity::<TextBoxState>(entity); + if state_entity.is_none() { + let state_entity = commands.spawn(TextBoxState::default()).id(); + widget_context.set_context_entity::<TextBoxState>(Some(entity), state_entity); + return false; + } + + let state_entity = state_entity.unwrap(); + + *styles = KStyle::default() + // Required styles + .with_style(KStyle { + render_command: RenderCommand::Layout.into(), + ..Default::default() + }) + // Apply any prop-given styles + .with_style(&*styles) + // If not set by props, apply these styles + .with_style(KStyle { + top: Units::Pixels(0.0).into(), + bottom: Units::Pixels(0.0).into(), + height: Units::Pixels(26.0).into(), + // cursor: CursorIcon::Text.into(), + ..Default::default() + }); + + let background_styles = KStyle { + background_color: StyleProp::Value(Color::rgba(0.176, 0.196, 0.215, 1.0)), + border_radius: Corner::all(5.0).into(), height: Units::Pixels(26.0).into(), - cursor: CursorIcon::Text.into(), + padding_left: Units::Pixels(5.0).into(), + padding_right: Units::Pixels(5.0).into(), ..Default::default() - }), - ); - - let background_styles = Style { - background_color: Color::new(0.176, 0.196, 0.215, 1.0).into(), - border_radius: Corner::all(5.0).into(), - height: Units::Pixels(26.0).into(), - padding_left: Units::Pixels(5.0).into(), - padding_right: Units::Pixels(5.0).into(), - ..Default::default() - }; - - let has_focus = context.create_state(Focus(false)).unwrap(); - - let mut current_value = value.clone(); - let cloned_on_change = on_change.clone(); - let cloned_has_focus = has_focus.clone(); - - props.on_event = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::CharInput { c } => { - if !cloned_has_focus.get().0 { - return; - } - if is_backspace(c) { - if !current_value.is_empty() { - current_value.truncate(current_value.len() - 1); - } - } else if !c.is_control() { - current_value.push(c); - } - if let Some(on_change) = cloned_on_change.as_ref() { - if let Ok(mut on_change) = on_change.0.write() { - on_change(ChangeEvent { - value: current_value.clone(), - }); - } + }; + + let current_value = text_box.value.clone(); + let cloned_on_change = on_change.clone(); + + *on_event = OnEvent::new( + move |In((event_dispatcher_context, mut event, _entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut state_query: Query<&mut TextBoxState>| { + match event.event_type { + EventType::CharInput { c } => { + let mut current_value = current_value.clone(); + let cloned_on_change = cloned_on_change.clone(); + if let Ok(state) = state_query.get(state_entity) { + if !state.focused { + return (event_dispatcher_context, event); + } + } else { + return (event_dispatcher_context, event); + } + if is_backspace(c) { + if !current_value.is_empty() { + current_value.truncate(current_value.len() - 1); + } + } else if !c.is_control() { + current_value.push(c); + } + cloned_on_change.set_value(current_value); + event.add_system(cloned_on_change); + } + EventType::Focus => { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.focused = true; + } + } + EventType::Blur => { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.focused = false; + } + } + _ => {} + } + (event_dispatcher_context, event) + }, + ); + + let parent_id = Some(entity); + rsx! { + <BackgroundBundle styles={background_styles}> + <ClipBundle styles={KStyle { + height: Units::Pixels(26.0).into(), + padding_left: StyleProp::Value(Units::Stretch(0.0)), + padding_right: StyleProp::Value(Units::Stretch(0.0)), + padding_bottom: StyleProp::Value(Units::Stretch(1.0)), + padding_top: StyleProp::Value(Units::Stretch(1.0)), + ..Default::default() + }}> + <TextWidgetBundle + text={TextProps { + content: text_box.value.clone(), + size: 14.0, + line_height: Some(18.0), + ..Default::default() + }} + // styles={text_styles} + /> + </ClipBundle> + </BackgroundBundle> } + + return true; } - EventType::Focus => cloned_has_focus.set(Focus(true)), - EventType::Blur => cloned_has_focus.set(Focus(false)), - _ => {} - })); - - let text_styles = if value.is_empty() || (has_focus.get().0 && value.is_empty()) { - Style { - color: Color::new(0.5, 0.5, 0.5, 1.0).into(), - ..Style::default() - } - } else { - Style::default() - }; - - let value = if value.is_empty() { - placeholder.unwrap_or_else(|| value.clone()) - } else { - value - }; - - rsx! { - <Background styles={Some(background_styles)}> - <Clip> - <Text - content={value} - size={14.0} - line_height={Some(22.0)} - styles={Some(text_styles)} - /> - </Clip> - </Background> } + + false } /// Checks if the given character contains the "Backspace" sequence diff --git a/src/widgets/texture_atlas.rs b/src/widgets/texture_atlas.rs index 1eade06..7396ca5 100644 --- a/src/widgets/texture_atlas.rs +++ b/src/widgets/texture_atlas.rs @@ -1,34 +1,12 @@ -use kayak_core::OnLayout; +use bevy::prelude::{Bundle, Changed, Component, Entity, Handle, Image, In, Or, Query, Vec2, With}; -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{Style, StyleProp}, - widget, Children, OnEvent, WidgetProps, +use crate::{ + context::{Mounted, WidgetName}, + prelude::WidgetContext, + styles::{KStyle, RenderCommand, StyleProp}, + widget::Widget, }; -/// Props used by the [`NinePatch`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TextureAtlasProps { - /// The handle to image - pub handle: u16, - /// The position of the tile (in pixels) - pub position: (f32, f32), - /// The size of the tile (in pixels) - pub tile_size: (f32, f32), - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, -} - -#[widget] /// A widget that renders a texture atlas /// Allows for the use of a partial square of an image such as in a sprite sheet /// @@ -38,25 +16,60 @@ pub struct TextureAtlasProps { /// /// | Common Prop | Accepted | /// | :---------: | :------: | -/// | `children` | ✅ | +/// | `children` | | /// | `styles` | ✅ | /// | `on_event` | ✅ | /// | `on_layout` | ✅ | /// | `focusable` | ✅ | /// -pub fn TextureAtlas(props: TextureAtlasProps) { - props.styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::TextureAtlas { - position: props.position, - size: props.tile_size, - handle: props.handle, - }), - ..props.styles.clone().unwrap_or_default() - }); - - rsx! { - <> - {children} - </> +#[derive(Component, Default, Debug)] +pub struct TextureAtlas { + /// The handle to image + pub handle: Handle<Image>, + /// The position of the tile (in pixels) + pub position: Vec2, + /// The size of the tile (in pixels) + pub tile_size: Vec2, +} + +impl Widget for TextureAtlas {} + +#[derive(Bundle)] +pub struct TextureAtlasBundle { + pub atlas: TextureAtlas, + pub styles: KStyle, + pub widget_name: WidgetName, +} + +impl Default for TextureAtlasBundle { + fn default() -> Self { + Self { + atlas: Default::default(), + styles: Default::default(), + widget_name: TextureAtlas::default().get_name(), + } } } + +pub fn update_texture_atlas( + In((_widget_context, entity)): In<(WidgetContext, Entity)>, + mut query: Query< + (&mut KStyle, &TextureAtlas), + Or<(Changed<TextureAtlas>, Changed<KStyle>, With<Mounted>)>, + >, +) -> bool { + if let Ok((mut styles, texture_atlas)) = query.get_mut(entity) { + *styles = KStyle { + render_command: StyleProp::Value(RenderCommand::TextureAtlas { + position: texture_atlas.position, + size: texture_atlas.tile_size, + handle: texture_atlas.handle.clone_weak(), + }), + ..styles.clone() + }; + + return true; + } + + false +} diff --git a/src/widgets/tooltip.rs b/src/widgets/tooltip.rs deleted file mode 100644 index 451faa2..0000000 --- a/src/widgets/tooltip.rs +++ /dev/null @@ -1,275 +0,0 @@ -use crate::core::{ - render_command::RenderCommand, - rsx, - styles::{PositionType, Style, StyleProp, Units}, - widget, Bound, Children, Color, EventType, MutableBound, OnEvent, WidgetProps, -}; -use std::sync::Arc; - -use crate::widgets::{Background, Clip, Element, If, Text}; - -/// Data provided by a [`TooltipProvider`] used to control a tooltip -#[derive(Clone, PartialEq, Debug, Default)] -pub struct TooltipData { - /// The anchor coordinates in pixels (x, y) - pub anchor: (f32, f32), - /// The size of the tooltip in pixels (width, height) - pub size: Option<(f32, f32)>, - /// The text to display - pub text: String, - /// Whether the tooltip is visible or not - pub visible: bool, -} - -/// Props used by the [`TooltipProvider`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TooltipProviderProps { - /// The position of the containing rect (used to layout the tooltip) - pub position: (f32, f32), - /// The size of the containing rect (used to layout the tooltip) - pub size: (f32, f32), - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, -} - -/// Props used by the [`TooltipProvider`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct TooltipConsumerProps { - /// The position at which to anchor the tooltip (in pixels) - /// - /// If `None`, the tooltip will follow the cursor - pub anchor: Option<(f32, f32)>, - /// The size of the tooltip - /// - /// If `None`, the tooltip will be automatically sized - pub size: Option<(f32, f32)>, - /// The text to display in the tooltip - pub text: String, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, -} - -#[widget] -/// A widget that provides a context for managing a tooltip -/// -/// This widget creates a single tooltip that can be controlled by any descendant [`TooltipConsumer`], -/// or by creating a consumer for [`TooltipData`] -/// -/// # Props -/// -/// __Type:__ [`TooltipProviderProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ⌠| -/// | `focusable` | ⌠| -/// -/// # Styles -/// -/// This widget accepts all styles and affects the actual tooltip container. The `background_color` -/// and `color` styles, however, apply directly to the tooltip itself. -/// -/// # Examples -/// -/// ``` -/// # use kayak_ui::core::{rsx, widget}; -/// -/// #[widget] -/// fn MyWidget() { -/// rsx! { -/// <> -/// <TooltipProvider size={Some(1280.0, 720.0)}> -/// // ... -/// <TooltipConsumer text={"Tooltip A".to_string()}> -/// // ... -/// </TooltipConsumer> -/// <TooltipConsumer text={"Tooltip B".to_string()}> -/// // ... -/// </TooltipConsumer> -/// // ... -/// </TooltipProvider> -/// </> -/// } -/// } -/// ``` -pub fn TooltipProvider(props: TooltipProviderProps) { - let TooltipProviderProps { position, size, .. } = props; - const WIDTH: f32 = 150.0; - const HEIGHT: f32 = 18.0; - const PADDING: (f32, f32) = (10.0, 5.0); - - let tooltip = context.create_provider(TooltipData::default()); - let TooltipData { - anchor, - size: tooltip_size, - text, - visible, - .. - } = tooltip.get(); - let tooltip_size = tooltip_size.unwrap_or((WIDTH, HEIGHT)); - - props.styles = Some( - Style::default() - .with_style(Style { - left: StyleProp::Value(Units::Pixels(position.0)), - top: StyleProp::Value(Units::Pixels(position.1)), - ..Default::default() - }) - .with_style(&props.styles) - .with_style(Style { - width: StyleProp::Value(Units::Pixels(size.0)), - height: StyleProp::Value(Units::Pixels(size.1)), - ..Default::default() - }), - ); - - let base_styles = props.styles.clone().unwrap(); - let mut tooltip_styles = Style { - position_type: StyleProp::Value(PositionType::SelfDirected), - background_color: StyleProp::select(&[ - &base_styles.background_color, - &Color::new(0.13, 0.15, 0.17, 0.85).into(), - ]) - .clone(), - width: StyleProp::Value(Units::Pixels(tooltip_size.0)), - height: StyleProp::Value(Units::Pixels(tooltip_size.1)), - ..Style::default() - }; - - if anchor.0 < size.0 / 2.0 { - tooltip_styles.left = StyleProp::Value(Units::Pixels(anchor.0 + PADDING.0)); - } else { - // TODO: Replace with `right` (currently not working properly) - tooltip_styles.left = StyleProp::Value(Units::Pixels(anchor.0 - tooltip_size.0)); - } - - if anchor.1 < size.1 / 2.0 { - tooltip_styles.top = StyleProp::Value(Units::Pixels(anchor.1 + PADDING.1)); - } else { - // TODO: Replace with `bottom` (currently not working properly) - tooltip_styles.top = StyleProp::Value(Units::Pixels(anchor.1 - tooltip_size.1)); - } - - let text_styles = Style { - width: StyleProp::Value(Units::Pixels(tooltip_size.0)), - height: StyleProp::Value(Units::Pixels(tooltip_size.1)), - color: StyleProp::select(&[&base_styles.color, &Color::WHITE.into()]).clone(), - ..Style::default() - }; - - rsx! { - <> - <Element> - {children} - </Element> - <If condition={visible}> - <Background styles={Some(tooltip_styles)}> - <Clip> - <Text content={text} size={12.0} styles={Some(text_styles)} /> - </Clip> - </Background> - </If> - </> - } -} - -#[widget] -/// A widget that consumes the [`TooltipData`] from a [`TooltipProvider`], providing a -/// convenient way to apply a tooltip over its children. -/// -/// # Props -/// -/// __Type:__ [`TooltipConsumerProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `focusable` | ⌠| -/// -/// # Examples -/// ``` -/// # use kayak_ui::core::{rsx, widget}; -/// -/// #[widget] -/// fn MyWidget() { -/// rsx! { -/// <> -/// <TooltipProvider size={Some(1280.0, 720.0)}> -/// // ... -/// <TooltipConsumer text={"Tooltip A".to_string()}> -/// // ... -/// </TooltipConsumer> -/// <TooltipConsumer text={"Tooltip B".to_string()}> -/// // ... -/// </TooltipConsumer> -/// // ... -/// </TooltipProvider> -/// </> -/// } -/// } -/// ``` -pub fn TooltipConsumer(props: TooltipConsumerProps) { - let TooltipConsumerProps { - anchor, size, text, .. - } = props.clone(); - props.styles = Some( - Style::default() - .with_style(Style { - render_command: StyleProp::Value(RenderCommand::Clip), - ..Default::default() - }) - .with_style(&props.styles) - .with_style(Style { - width: StyleProp::Value(Units::Auto), - height: StyleProp::Value(Units::Auto), - ..Default::default() - }), - ); - - let data = context - .create_consumer::<TooltipData>() - .expect("TooltipConsumer requires TooltipProvider as an ancestor"); - - let text = Arc::new(text); - props.on_event = Some(OnEvent::new(move |ctx, event| match event.event_type { - EventType::MouseIn(..) => { - let mut state = data.get(); - state.visible = true; - state.text = (*text).clone(); - state.size = size; - data.set(state); - } - EventType::Hover(..) => { - let mut state = data.get(); - state.anchor = anchor.unwrap_or(ctx.last_mouse_position()); - data.set(state); - } - EventType::MouseOut(..) => { - let mut state = data.get(); - // Set hidden only if the tooltip's text matches this consumer's - // Otherwise, it likely got picked up by another widget and should be kept visible - state.visible = false || state.text != *text; - data.set(state); - } - _ => {} - })); - - rsx! { - <> - {children} - </> - } -} diff --git a/src/widgets/window.rs b/src/widgets/window.rs index b97e50c..4218956 100644 --- a/src/widgets/window.rs +++ b/src/widgets/window.rs @@ -1,156 +1,192 @@ -use crate::core::{ - color::Color, - render_command::RenderCommand, - rsx, - styles::{Corner, Edge, PositionType, Style, StyleProp, Units}, - use_state, widget, Children, EventType, OnEvent, WidgetProps, +use bevy::prelude::{ + Bundle, Changed, Color, Commands, Component, Entity, In, Or, Query, Vec2, With, }; -use kayak_core::{CursorIcon, OnLayout}; -use crate::widgets::{Background, Clip, Element, Text}; +use crate::{ + children::KChildren, + context::{Mounted, WidgetName}, + event::{Event, EventType}, + event_dispatcher::EventDispatcherContext, + on_event::OnEvent, + prelude::WidgetContext, + styles::{Corner, Edge, KStyle, PositionType, RenderCommand, StyleProp, Units}, + widget::Widget, +}; + +use super::{ + background::BackgroundBundle, + clip::ClipBundle, + text::{TextProps, TextWidgetBundle}, +}; -/// Props used by the [`Window`] widget -#[derive(WidgetProps, Default, Debug, PartialEq, Clone)] -pub struct WindowProps { +#[derive(Component, Debug, Default)] +pub struct KWindow { /// If true, allows the window to be draggable by its title bar pub draggable: bool, /// The position at which to display the window in pixels - pub position: (f32, f32), + pub position: Vec2, /// The size of the window in pixels - pub size: (f32, f32), + pub size: Vec2, /// The text to display in the window's title bar pub title: String, - #[prop_field(Styles)] - pub styles: Option<Style>, - #[prop_field(Children)] - pub children: Option<Children>, - #[prop_field(OnEvent)] - pub on_event: Option<OnEvent>, - #[prop_field(OnLayout)] - pub on_layout: Option<OnLayout>, - #[prop_field(Focusable)] - pub focusable: Option<bool>, + + pub is_dragging: bool, + pub offset: Vec2, + pub title_bar_entity: Option<Entity>, + // pub children: Vec<Entity>, } -#[widget] -/// A widget that renders a window-like container element -/// -/// # Props -/// -/// __Type:__ [`WindowProps`] -/// -/// | Common Prop | Accepted | -/// | :---------: | :------: | -/// | `children` | ✅ | -/// | `styles` | ✅ | -/// | `on_event` | ✅ | -/// | `on_layout` | ✅ | -/// | `focusable` | ✅ | -/// -pub fn Window(props: WindowProps) { - let WindowProps { - draggable, - position, - size, - title, - .. - } = props.clone(); +impl Widget for KWindow {} - let (is_dragging, set_is_dragging, ..) = use_state!(false); - let (offset, set_offset, ..) = use_state!((0.0, 0.0)); - let (pos, set_pos, ..) = use_state!(position); +#[derive(Bundle)] +pub struct WindowBundle { + pub window: KWindow, + pub styles: KStyle, + pub children: KChildren, + pub widget_name: WidgetName, +} - let drag_handler = if draggable { - Some(OnEvent::new(move |ctx, event| match event.event_type { - EventType::MouseDown(data) => { - ctx.capture_cursor(event.current_target); - set_is_dragging(true); - set_offset((pos.0 - data.position.0, pos.1 - data.position.1)); - } - EventType::MouseUp(..) => { - ctx.release_cursor(event.current_target); - set_is_dragging(false); - } - EventType::Hover(data) => { - if is_dragging { - set_pos((offset.0 + data.position.0, offset.1 + data.position.1)); - } - } - _ => {} - })) - } else { - None - }; +impl Default for WindowBundle { + fn default() -> Self { + Self { + window: Default::default(), + styles: Default::default(), + children: Default::default(), + widget_name: KWindow::default().get_name(), + } + } +} - props.styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.125, 0.125, 0.125, 1.0)), - border_color: StyleProp::Value(Color::new(0.0781, 0.0898, 0.101, 1.0)), - border: StyleProp::Value(Edge::all(4.0)), - border_radius: StyleProp::Value(Corner::all(5.0)), - render_command: StyleProp::Value(RenderCommand::Quad), - position_type: StyleProp::Value(PositionType::SelfDirected), - left: StyleProp::Value(Units::Pixels(pos.0)), - top: StyleProp::Value(Units::Pixels(pos.1)), - width: StyleProp::Value(Units::Pixels(size.0)), - height: StyleProp::Value(Units::Pixels(size.1)), - max_width: StyleProp::Value(Units::Pixels(size.0)), - max_height: StyleProp::Value(Units::Pixels(size.1)), - ..props.styles.clone().unwrap_or_default() - }); +pub fn window_update( + In((widget_context, window_entity)): In<(WidgetContext, Entity)>, + mut commands: Commands, + mut query: Query< + (&mut KStyle, &KChildren, &mut KWindow), + Or<( + Changed<KWindow>, + Changed<KStyle>, + Changed<KChildren>, + With<Mounted>, + )>, + >, +) -> bool { + let mut has_changed = false; + if let Ok((mut window_style, children, mut window)) = query.get_mut(window_entity) { + *window_style = KStyle { + background_color: StyleProp::Value(Color::rgba(0.125, 0.125, 0.125, 1.0)), + border_color: StyleProp::Value(Color::rgba(0.0781, 0.0898, 0.101, 1.0)), + border: StyleProp::Value(Edge::all(4.0)), + border_radius: StyleProp::Value(Corner::all(5.0)), + render_command: StyleProp::Value(RenderCommand::Quad), + position_type: StyleProp::Value(PositionType::SelfDirected), + left: StyleProp::Value(Units::Pixels(window.position.x)), + top: StyleProp::Value(Units::Pixels(window.position.y)), + width: StyleProp::Value(Units::Pixels(window.size.x)), + height: StyleProp::Value(Units::Pixels(window.size.y)), + min_width: StyleProp::Value(Units::Pixels(window.size.x)), + min_height: StyleProp::Value(Units::Pixels(window.size.y)), + ..window_style.clone() + }; - let clip_styles = Style { - padding: StyleProp::Value(Edge::all(Units::Pixels(5.0))), - width: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Stretch(1.0)), - max_width: StyleProp::Value(Units::Pixels(size.0)), - max_height: StyleProp::Value(Units::Pixels(size.1)), - ..Style::default() - }; + if window.title_bar_entity.is_none() { + let title = window.title.clone(); - let cursor = if draggable { - if is_dragging { - CursorIcon::Grabbing - } else { - CursorIcon::Grab - } - } else { - CursorIcon::Default - }; + let mut title_children = KChildren::new(); + // Spawn title children + let title_entity = commands + .spawn(TextWidgetBundle { + text: TextProps { + content: title.clone(), + size: 16.0, + line_height: Some(25.0), + ..Default::default() + }, + styles: KStyle { + height: StyleProp::Value(Units::Pixels(25.0)), + ..KStyle::default() + }, + ..Default::default() + }) + .id(); + title_children.add(title_entity); - let title_background_styles = Style { - background_color: StyleProp::Value(Color::new(0.0781, 0.0898, 0.101, 1.0)), - border_radius: StyleProp::Value(Corner::all(5.0)), - cursor: cursor.into(), - height: StyleProp::Value(Units::Pixels(24.0)), - width: StyleProp::Value(Units::Stretch(1.0)), - left: StyleProp::Value(Units::Pixels(0.0)), - right: StyleProp::Value(Units::Pixels(0.0)), - top: StyleProp::Value(Units::Pixels(0.0)), - bottom: StyleProp::Value(Units::Pixels(0.0)), - padding_left: StyleProp::Value(Units::Pixels(5.0)), - ..Style::default() - }; + let title_background_entity = commands + .spawn(BackgroundBundle { + styles: KStyle { + render_command: StyleProp::Value(RenderCommand::Quad), + background_color: StyleProp::Value(Color::rgba(0.0781, 0.0898, 0.101, 1.0)), + border_radius: StyleProp::Value(Corner::all(5.0)), + height: StyleProp::Value(Units::Pixels(24.0)), + width: StyleProp::Value(Units::Stretch(1.0)), + left: StyleProp::Value(Units::Pixels(0.0)), + right: StyleProp::Value(Units::Pixels(0.0)), + top: StyleProp::Value(Units::Pixels(0.0)), + bottom: StyleProp::Value(Units::Pixels(0.0)), + padding_left: StyleProp::Value(Units::Pixels(5.0)), + ..KStyle::default() + }, + children: title_children, + ..BackgroundBundle::default() + }) + .id(); + window.title_bar_entity = Some(title_background_entity); - let title_text_styles = Style { - height: StyleProp::Value(Units::Pixels(25.0)), - cursor: StyleProp::Inherit, - ..Style::default() - }; + if window.draggable { + commands + .entity(window.title_bar_entity.unwrap()) + .insert(OnEvent::new( + move |In((mut event_dispatcher_context, event, entity)): In<( + EventDispatcherContext, + Event, + Entity, + )>, + mut query: Query<&mut KWindow>| { + if let Ok(mut window) = query.get_mut(window_entity) { + match event.event_type { + EventType::MouseDown(data) => { + event_dispatcher_context.capture_cursor(entity); + window.is_dragging = true; + window.offset = Vec2::new( + window.position.x - data.position.0, + window.position.y - data.position.1, + ); + window.title_bar_entity = None; + } + EventType::MouseUp(..) => { + event_dispatcher_context.release_cursor(entity); + window.is_dragging = false; + window.title_bar_entity = None; + } + EventType::Hover(data) => { + if window.is_dragging { + window.position = Vec2::new( + window.offset.x + data.position.0, + window.offset.y + data.position.1, + ); + window.title_bar_entity = None; + } + } + _ => {} + } + } + (event_dispatcher_context, event) + }, + )); + } + widget_context.add_widget(Some(window_entity), window.title_bar_entity.unwrap()); - let content_styles = Style { - padding: StyleProp::Value(Edge::all(Units::Pixels(10.0))), - ..Style::default() - }; + let mut clip_bundle = ClipBundle { + children: children.clone(), + ..ClipBundle::default() + }; + clip_bundle.styles.padding = StyleProp::Value(Edge::all(Units::Pixels(10.0))); - let title = title.clone(); - rsx! { - <Clip styles={Some(clip_styles)}> - <Background on_event={drag_handler} styles={Some(title_background_styles)}> - <Text styles={Some(title_text_styles)} size={16.0} content={title} /> - </Background> - <Element styles={Some(content_styles)}> - {children} - </Element> - </Clip> + let clip_entity = commands.spawn(clip_bundle).id(); + widget_context.add_widget(Some(window_entity), clip_entity); + // let children = widget_context.get_children(window_entity); + has_changed = true; + } } + + has_changed } diff --git a/bevy_kayak_renderer/src/lib.rs b/src/window_size.rs similarity index 66% rename from bevy_kayak_renderer/src/lib.rs rename to src/window_size.rs index 9d98ad2..4b17d24 100644 --- a/bevy_kayak_renderer/src/lib.rs +++ b/src/window_size.rs @@ -3,28 +3,11 @@ use bevy::{ window::{WindowCreated, WindowResized}, }; -pub mod camera; -pub mod render; - -pub use camera::*; - -#[derive(Default)] -pub struct BevyKayakRendererPlugin; - -impl Plugin for BevyKayakRendererPlugin { - fn build(&self, app: &mut bevy::prelude::App) { - app.add_system(update_window_size) - .init_resource::<WindowSize>() - .add_plugin(render::BevyKayakUIRenderPlugin) - .add_plugin(camera::KayakUICameraPlugin); - } -} - /// Tracks the bevy window size. -#[derive(Default, Debug, Clone, Copy, PartialEq)] +#[derive(Resource, Default, Debug, Clone, Copy, PartialEq)] pub struct WindowSize(pub f32, pub f32); -fn update_window_size( +pub fn update_window_size( mut window_resized_events: EventReader<WindowResized>, mut window_created_events: EventReader<WindowCreated>, windows: Res<Windows>, @@ -59,11 +42,3 @@ fn update_window_size( } } } - -#[derive(Debug, Clone, Copy, Default)] -pub struct Corner<T> { - pub top_left: T, - pub top_right: T, - pub bottom_left: T, - pub bottom_right: T, -} -- GitLab