├── crates ├── craft_renderer │ ├── src │ │ ├── color.rs │ │ ├── image_adapter.rs │ │ ├── lib.rs │ │ ├── blank_renderer.rs │ │ ├── text_renderer_data.rs │ │ └── renderer_type.rs │ └── Cargo.toml ├── craft_core │ ├── src │ │ ├── tests │ │ │ └── mod.rs │ │ ├── utils │ │ │ ├── mod.rs │ │ │ └── cloneable_any.rs │ │ ├── layout │ │ │ └── mod.rs │ │ ├── style │ │ │ ├── mod.rs │ │ │ └── style_flags.rs │ │ ├── accessibility │ │ │ ├── mod.rs │ │ │ ├── deactivation_handler.rs │ │ │ ├── activation_handler.rs │ │ │ └── access_handler.rs │ │ ├── devtools │ │ │ ├── mod.rs │ │ │ ├── dev_tools_colors.rs │ │ │ ├── dev_tools_component.rs │ │ │ └── tree_window.rs │ │ ├── animations │ │ │ └── mod.rs │ │ ├── reactive │ │ │ ├── mod.rs │ │ │ ├── element_id.rs │ │ │ ├── state_store.rs │ │ │ ├── element_state_store.rs │ │ │ ├── reactive_tree.rs │ │ │ └── fiber_tree.rs │ │ ├── elements │ │ │ ├── element_states.rs │ │ │ ├── stateful_element.rs │ │ │ ├── mod.rs │ │ │ ├── empty.rs │ │ │ ├── base_element_state.rs │ │ │ ├── font.rs │ │ │ ├── element_pre_order_iterator.rs │ │ │ ├── image.rs │ │ │ ├── thumb.rs │ │ │ ├── tinyvg.rs │ │ │ ├── element_data.rs │ │ │ └── overlay.rs │ │ ├── events │ │ │ ├── mouse_wheel.rs │ │ │ ├── update_queue_entry.rs │ │ │ ├── internal.rs │ │ │ ├── event_handlers.rs │ │ │ └── mod.rs │ │ ├── components │ │ │ ├── props.rs │ │ │ ├── mod.rs │ │ │ ├── web_link.rs │ │ │ └── update_result.rs │ │ ├── text │ │ │ ├── text_context.rs │ │ │ └── mod.rs │ │ ├── options.rs │ │ ├── wasm_queue.rs │ │ └── view_introspection.rs │ ├── LICENSE │ └── Cargo.toml ├── craft_primitives │ ├── src │ │ ├── color.rs │ │ ├── lib.rs │ │ ├── geometry │ │ │ ├── cornerside.rs │ │ │ ├── point.rs │ │ │ ├── mod.rs │ │ │ ├── side.rs │ │ │ ├── size.rs │ │ │ ├── trblrectangle.rs │ │ │ ├── corner.rs │ │ │ ├── element_box.rs │ │ │ └── rectangle.rs │ │ └── color_brush.rs │ └── Cargo.toml ├── syntect_dumper │ ├── pack.dump │ ├── theme_pack.dump │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── craft_dylib │ ├── src │ │ └── lib.rs │ ├── Cargo.toml │ └── LICENSE ├── craft_logger │ ├── src │ │ └── lib.rs │ ├── Cargo.toml │ └── LICENSE ├── craft │ ├── src │ │ └── lib.rs │ ├── LICENSE │ └── Cargo.toml ├── craft_resource_manager │ ├── src │ │ ├── resource_type.rs │ │ ├── resource_event.rs │ │ ├── tinyvg_resource.rs │ │ ├── resource_data.rs │ │ ├── resource.rs │ │ ├── image.rs │ │ └── identifier.rs │ └── Cargo.toml └── craft_runtime │ └── Cargo.toml ├── images ├── tour.png ├── counter.png ├── ani_list.png ├── svg_to_tvg.png └── text_editor.png ├── .gitignore ├── website ├── src │ ├── docs │ │ ├── markdown │ │ │ ├── hello_world │ │ │ │ └── intro.md │ │ │ ├── how_to_contribute.md │ │ │ └── installation.md │ │ ├── mod.rs │ │ ├── installation.rs │ │ ├── how_to_contribute.rs │ │ ├── styling.rs │ │ ├── hello_world.rs │ │ ├── state_management.rs │ │ └── markdown_viewer.rs │ ├── link.rs │ ├── web_link.rs │ ├── theme.rs │ ├── router.rs │ ├── navbar.rs │ └── main.rs ├── assets │ ├── brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg │ ├── code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg │ ├── devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg │ ├── view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg │ └── electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg ├── wasm-build.sh ├── index.html └── Cargo.toml ├── fonts ├── Roboto-Bold.ttf ├── Roboto-Medium.ttf ├── Roboto-Regular.ttf ├── Roboto-SemiBold.ttf └── OFL.txt ├── examples ├── tour │ ├── tiger.tvg │ └── Cargo.toml ├── text │ ├── Cargo.toml │ └── main.rs ├── events │ ├── Cargo.toml │ └── main.rs ├── overlay │ ├── Cargo.toml │ └── main.rs ├── animations │ ├── Cargo.toml │ └── main.rs ├── util │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── counter │ ├── counter_android.rs │ ├── Cargo.toml │ └── main.rs ├── custom_event_loop │ ├── triangle.wgsl │ ├── Cargo.toml │ └── main.rs └── request │ ├── Cargo.toml │ └── ani_list.rs ├── wasm-build.sh ├── index.html ├── LICENSE ├── Cargo.toml ├── rustfmt.toml ├── README.md └── .github └── workflows └── ci.yml /crates/craft_renderer/src/color.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/craft_core/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/craft_core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cloneable_any; -------------------------------------------------------------------------------- /images/tour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/images/tour.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | .idea 3 | .vs 4 | .vscode 5 | 6 | # Binaries 7 | /target 8 | -------------------------------------------------------------------------------- /crates/craft_core/src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod layout_context; 2 | pub mod layout_item; 3 | -------------------------------------------------------------------------------- /images/counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/images/counter.png -------------------------------------------------------------------------------- /website/src/docs/markdown/hello_world/intro.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | Coming soon! 4 | -------------------------------------------------------------------------------- /fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /images/ani_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/images/ani_list.png -------------------------------------------------------------------------------- /images/svg_to_tvg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/images/svg_to_tvg.png -------------------------------------------------------------------------------- /examples/tour/tiger.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/examples/tour/tiger.tvg -------------------------------------------------------------------------------- /fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /images/text_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/images/text_editor.png -------------------------------------------------------------------------------- /crates/craft_primitives/src/color.rs: -------------------------------------------------------------------------------- 1 | pub use peniko::color::palette; 2 | pub use peniko::Color; 3 | -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /fonts/Roboto-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/fonts/Roboto-SemiBold.ttf -------------------------------------------------------------------------------- /crates/syntect_dumper/pack.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/crates/syntect_dumper/pack.dump -------------------------------------------------------------------------------- /crates/craft_dylib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports, clippy::single_component_path_imports)] 2 | use craft_core; 3 | -------------------------------------------------------------------------------- /crates/craft_logger/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use tracing::{debug, error, info, warn, Level}; 2 | 3 | pub use tracing::span; 4 | -------------------------------------------------------------------------------- /crates/syntect_dumper/theme_pack.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/crates/syntect_dumper/theme_pack.dump -------------------------------------------------------------------------------- /crates/craft_core/src/style/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod style_flags; 2 | mod styles; 3 | mod taffy_conversions; 4 | 5 | pub use styles::*; 6 | -------------------------------------------------------------------------------- /crates/craft/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use craft_core::*; 2 | 3 | #[cfg(feature = "dynamic_linking")] 4 | #[allow(unused_imports)] 5 | use craft_dylib; 6 | -------------------------------------------------------------------------------- /crates/craft_core/src/accessibility/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod access_handler; 2 | pub(crate) mod activation_handler; 3 | pub(crate) mod deactivation_handler; 4 | -------------------------------------------------------------------------------- /crates/craft_core/src/devtools/mod.rs: -------------------------------------------------------------------------------- 1 | mod dev_tools_colors; 2 | pub(crate) mod dev_tools_component; 3 | mod dev_tools_element; 4 | mod tree_window; 5 | mod layout_window; 6 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/resource_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 2 | pub enum ResourceType { 3 | Image, 4 | Font, 5 | TinyVg, 6 | } 7 | -------------------------------------------------------------------------------- /website/assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/website/assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg -------------------------------------------------------------------------------- /website/assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/website/assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg -------------------------------------------------------------------------------- /website/assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/website/assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg -------------------------------------------------------------------------------- /crates/craft_primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | 3 | pub mod geometry; 4 | mod color_brush; 5 | 6 | pub use color::Color; 7 | pub use color::palette; 8 | pub use color_brush::ColorBrush; -------------------------------------------------------------------------------- /website/assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/website/assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg -------------------------------------------------------------------------------- /website/assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craft-gui/craft/HEAD/website/assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg -------------------------------------------------------------------------------- /crates/craft_core/src/animations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod animation; 2 | 3 | pub use animation::Animation; 4 | pub use animation::KeyFrame; 5 | pub use animation::LoopAmount; 6 | pub use animation::TimingFunction; -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod element_id; 2 | pub(crate) mod fiber_tree; 3 | pub mod tree; 4 | 5 | pub mod element_state_store; 6 | pub(crate) mod reactive_tree; 7 | pub mod state_store; 8 | #[cfg(test)] 9 | mod tests; 10 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/element_states.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] 2 | pub enum ElementState { 3 | #[default] 4 | Normal, 5 | Hovered, 6 | Pressed, 7 | Disabled, 8 | Focused, 9 | } 10 | -------------------------------------------------------------------------------- /crates/craft_primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_primitives" 3 | version = "0.1.1" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | 8 | [dependencies.kurbo] 9 | workspace = true 10 | 11 | [dependencies.peniko] 12 | workspace = true 13 | 14 | [dependencies.dpi] 15 | workspace = true -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/cornerside.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | #[repr(usize)] 3 | pub enum CornerSide { 4 | Top = 0, 5 | Bottom = 1, 6 | } 7 | 8 | impl CornerSide { 9 | pub(crate) fn next(self) -> Self { 10 | unsafe { std::mem::transmute((self as usize + 1) & 1) } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/resource_event.rs: -------------------------------------------------------------------------------- 1 | use crate::resource::Resource; 2 | use crate::resource_type::ResourceType; 3 | use crate::ResourceIdentifier; 4 | 5 | #[derive(Debug)] 6 | pub enum ResourceEvent { 7 | Loaded(ResourceIdentifier, ResourceType, Resource), 8 | #[allow(dead_code)] 9 | UnLoaded(ResourceIdentifier), 10 | } 11 | -------------------------------------------------------------------------------- /crates/syntect_dumper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "syntect_dumper" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | 8 | [dependencies.reqwest] 9 | version = "0.12.20" 10 | features = ["blocking"] 11 | 12 | [dependencies.syntect] 13 | version = "5.2.0" 14 | features = ["dump-create"] 15 | 16 | [[bin]] 17 | name = "syntect-dumper" 18 | path = "src/main.rs" -------------------------------------------------------------------------------- /examples/text/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "text" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "text" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | util = { path = "../util" } 12 | 13 | [dependencies.craft] 14 | path = "../../crates/craft" 15 | default-features = false 16 | features = ["vello_renderer", "devtools", "http_client"] 17 | package = "craft_gui" 18 | -------------------------------------------------------------------------------- /examples/events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "events" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "events" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | util = { path = "../util" } 12 | 13 | [dependencies.craft] 14 | path = "../../crates/craft" 15 | default-features = false 16 | features = ["vello_renderer", "devtools", "accesskit"] 17 | package = "craft_gui" 18 | -------------------------------------------------------------------------------- /crates/craft_core/src/accessibility/deactivation_handler.rs: -------------------------------------------------------------------------------- 1 | use accesskit::DeactivationHandler; 2 | 3 | pub(crate) struct CraftDeactivationHandler {} 4 | 5 | impl DeactivationHandler for CraftDeactivationHandler { 6 | fn deactivate_accessibility(&mut self) {} 7 | } 8 | 9 | impl CraftDeactivationHandler { 10 | pub fn new() -> Self { 11 | CraftDeactivationHandler {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/overlay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "overlay" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "overlay" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | util = { path = "../util" } 12 | 13 | [dependencies.craft] 14 | path = "../../crates/craft" 15 | default-features = false 16 | features = ["vello_renderer", "devtools", "accesskit"] 17 | package = "craft_gui" 18 | -------------------------------------------------------------------------------- /examples/tour/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tour" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "tour" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | util = { path = "../util" } 12 | 13 | [dependencies.craft] 14 | path = "../../crates/craft" 15 | features = ["vello_renderer", "devtools", "accesskit", "system_fonts"] 16 | default-features = false 17 | package = "craft_gui" -------------------------------------------------------------------------------- /examples/animations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "animations" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "animations" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | util = { path = "../util" } 12 | 13 | [dependencies.craft] 14 | path = "../../crates/craft" 15 | default-features = false 16 | features = ["vello_renderer", "devtools", "accesskit"] 17 | package = "craft_gui" 18 | -------------------------------------------------------------------------------- /examples/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "util" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | tracing-subscriber = "0.3.19" 8 | tracing = { workspace = true } 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | tracing-web = "0.1.3" 12 | console_error_panic_hook = "0.1.7" 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | name = "util" 17 | path = "src/lib.rs" -------------------------------------------------------------------------------- /crates/craft_primitives/src/color_brush.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq)] 2 | pub struct ColorBrush { 3 | pub color: crate::Color, 4 | } 5 | 6 | impl ColorBrush { 7 | pub fn new(color: peniko::Color) -> Self { 8 | Self { color } 9 | } 10 | } 11 | 12 | impl Default for ColorBrush { 13 | fn default() -> Self { 14 | Self { 15 | color: peniko::Color::BLACK, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/counter/counter_android.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "android")] 2 | 3 | use craft::components::Component; 4 | use craft::{craft_main, AndroidApp, CraftOptions}; 5 | use util::setup_logging; 6 | 7 | #[path = "main.rs"] 8 | mod counter; 9 | use counter::Counter; 10 | 11 | #[unsafe(no_mangle)] 12 | pub unsafe fn android_main(app: AndroidApp) { 13 | setup_logging(); 14 | craft_main(Counter::component(), (), CraftOptions::basic("Counter"), app); 15 | } 16 | -------------------------------------------------------------------------------- /wasm-build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | 4 | #rustup target add wasm32-unknown-unknown 5 | #cargo install -f wasm-bindgen-cli 6 | #cargo install simple-http-server 7 | 8 | cargo build --target wasm32-unknown-unknown --package request 9 | 10 | wasm-bindgen target/wasm32-unknown-unknown/debug/ani_list.wasm --target web --no-typescript --out-dir target/generated --out-name request --debug --keep-debug 11 | 12 | simple-http-server . -c wasm,html,js -i --coep --coop --ip 0.0.0.0 13 | -------------------------------------------------------------------------------- /crates/craft_logger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_logging" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license-file = "LICENSE" 6 | description = "A logging library for the Craft project, providing structured logging and tracing capabilities." 7 | homepage = "https://craftgui.com/" 8 | repository = "https://github.com/craft-gui/craft" 9 | 10 | [dependencies] 11 | 12 | [dependencies.tracing] 13 | version = "0.1.40" 14 | default-features = false 15 | features = ["std"] -------------------------------------------------------------------------------- /crates/craft_renderer/src/image_adapter.rs: -------------------------------------------------------------------------------- 1 | use craft_resource_manager::image::ImageResource; 2 | use std::sync::Arc; 3 | 4 | pub struct ImageAdapter { 5 | image: Arc, 6 | } 7 | 8 | impl ImageAdapter { 9 | #[allow(dead_code)] 10 | pub fn new(image: Arc) -> Self { 11 | Self { image } 12 | } 13 | } 14 | 15 | impl AsRef<[u8]> for ImageAdapter { 16 | fn as_ref(&self) -> &[u8] { 17 | self.image.image.as_ref() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /website/wasm-build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | 4 | #rustup target add wasm32-unknown-unknown 5 | #cargo install -f wasm-bindgen-cli 6 | #cargo install simple-http-server 7 | 8 | cargo build --target wasm32-unknown-unknown --release 9 | 10 | wasm-bindgen ../target/wasm32-unknown-unknown/release/website.wasm --target web --no-typescript --out-dir dist --out-name website 11 | cp index.html dist/index.html 12 | simple-http-server dist -c wasm,html,js --try-file dist/index.html -i --coep --coop --ip 0.0.0.0 13 | -------------------------------------------------------------------------------- /website/src/docs/mod.rs: -------------------------------------------------------------------------------- 1 | use craft::elements::{Container, ElementStyles}; 2 | use craft::style::{Display, FlexDirection}; 3 | 4 | pub(crate) mod docs_component; 5 | pub(crate) mod installation; 6 | pub(crate) mod hello_world; 7 | pub(crate) mod state_management; 8 | pub(crate) mod styling; 9 | pub(crate) mod how_to_contribute; 10 | mod markdown_viewer; 11 | 12 | pub(crate) fn docs_template() -> Container { 13 | Container::new() 14 | .display(Display::Flex) 15 | .flex_direction(FlexDirection::Column) 16 | } -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/point.rs: -------------------------------------------------------------------------------- 1 | use dpi::PhysicalPosition; 2 | 3 | pub use kurbo::Point; 4 | 5 | /// A “Point-converter” extension trait. 6 | pub trait PointConverter { 7 | /// “Constructor” for a kurbo::Point 8 | fn new(x: f32, y: f32) -> Self; 9 | 10 | /// Convert a winit position into a kurbo::Point 11 | fn from_physical_pos(pos: PhysicalPosition) -> Self; 12 | 13 | ///// Convert a taffy::Point into a kurbo::Point 14 | //fn from_taffy_point(p: taffy::Point) -> Self; 15 | } 16 | -------------------------------------------------------------------------------- /examples/custom_event_loop/triangle.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4, 3 | }; 4 | 5 | @group(0) 6 | @binding(0) 7 | var transform: mat4x4; 8 | 9 | @vertex 10 | fn vs_main( 11 | @location(0) position: vec4, 12 | ) -> VertexOutput { 13 | var result: VertexOutput; 14 | result.position = transform * position; 15 | return result; 16 | } 17 | 18 | @fragment 19 | fn fs_main() -> @location(0) vec4 { 20 | return vec4(1.0, 0.0, 0.0, 1.0); 21 | } 22 | -------------------------------------------------------------------------------- /crates/craft_core/src/events/mouse_wheel.rs: -------------------------------------------------------------------------------- 1 | use winit::event::{DeviceId, MouseScrollDelta, TouchPhase}; 2 | 3 | #[derive(Clone, Copy, Debug)] 4 | pub struct MouseWheel { 5 | pub device_id: Option, 6 | pub delta: MouseScrollDelta, 7 | pub phase: TouchPhase, 8 | } 9 | 10 | impl MouseWheel { 11 | pub fn new(device_id: Option, delta: MouseScrollDelta, phase: TouchPhase) -> Self { 12 | Self { 13 | device_id, 14 | delta, 15 | phase, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | edition = "2024" 5 | default-run = "counter" 6 | 7 | [lib] 8 | name = "counter_android" 9 | path = "counter_android.rs" 10 | crate-type = ["cdylib"] 11 | 12 | [[bin]] 13 | name = "counter" 14 | path = "main.rs" 15 | 16 | [dependencies] 17 | util = { path = "../util" } 18 | 19 | [dependencies.craft] 20 | path = "../../crates/craft" 21 | default-features = false 22 | features = ["vello_renderer", "devtools", "accesskit", "system_fonts"] 23 | package = "craft_gui" -------------------------------------------------------------------------------- /crates/craft_core/src/devtools/dev_tools_colors.rs: -------------------------------------------------------------------------------- 1 | use crate::Color; 2 | 3 | pub(crate) const FIELD_NAME_COLOR: Color = Color::from_rgb8(117, 191, 255); 4 | pub(crate) const FIELD_VALUE_COLOR: Color = Color::from_rgb8(255, 125, 233); 5 | pub(crate) const CONTAINER_BACKGROUND_COLOR: Color = Color::from_rgb8(24, 24, 26); 6 | pub(crate) const ROW_BACKGROUND_COLOR: Color = Color::from_rgb8(35, 35, 39); 7 | pub(crate) const SELECTED_ROW_BACKGROUND_COLOR: Color = Color::from_rgb8(45, 45, 90); 8 | pub(crate) const BORDER_COLOR: Color = Color::from_rgb8(56, 56, 61); 9 | -------------------------------------------------------------------------------- /crates/craft_dylib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_dylib" 3 | description = "Allow Craft to be compiled as a dynamic library." 4 | version = "0.1.1" 5 | edition = "2024" 6 | resolver = "2" 7 | license-file = "LICENSE" 8 | homepage = "https://craftgui.com/" 9 | repository = "https://github.com/craft-gui/craft" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | [lib] 13 | crate-type = ["dylib"] 14 | 15 | [dependencies] 16 | craft_core = { path = "../craft_core", default-features = false, version = "0.1.1" } -------------------------------------------------------------------------------- /crates/craft_core/src/accessibility/activation_handler.rs: -------------------------------------------------------------------------------- 1 | use accesskit::{ActivationHandler, TreeUpdate}; 2 | 3 | pub(crate) struct CraftActivationHandler { 4 | tree_update: Option, 5 | } 6 | 7 | impl ActivationHandler for CraftActivationHandler { 8 | fn request_initial_tree(&mut self) -> Option { 9 | Some(self.tree_update.take().unwrap()) 10 | } 11 | } 12 | 13 | impl CraftActivationHandler { 14 | pub fn new(tree_update: Option) -> Self { 15 | CraftActivationHandler { tree_update } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/element_id.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | thread_local! { 4 | static THREAD_LOCAL_ELEMENT_ID: Cell = const { Cell::new(0) }; 5 | } 6 | 7 | pub fn create_unique_element_id() -> u64 { 8 | THREAD_LOCAL_ELEMENT_ID.with(|counter| { 9 | let id = counter.get(); 10 | counter.set(id + 1); 11 | id 12 | }) 13 | } 14 | 15 | pub fn reset_unique_element_id() { 16 | THREAD_LOCAL_ELEMENT_ID.with(|counter| { 17 | counter.set(0); 18 | }) 19 | } 20 | 21 | pub fn get_current_element_id_counter() -> u64 { 22 | THREAD_LOCAL_ELEMENT_ID.get() 23 | } 24 | -------------------------------------------------------------------------------- /crates/craft_renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | 3 | #[allow(clippy::module_inception)] 4 | pub mod renderer; 5 | 6 | #[cfg(feature = "vello_renderer")] 7 | pub mod vello; 8 | 9 | #[cfg(feature = "vello_cpu_renderer")] 10 | pub mod vello_cpu; 11 | 12 | pub mod blank_renderer; 13 | mod image_adapter; 14 | pub(crate) mod tinyvg_helpers; 15 | #[cfg(feature = "vello_hybrid_renderer")] 16 | pub mod vello_hybrid; 17 | pub mod text_renderer_data; 18 | mod renderer_type; 19 | 20 | pub use renderer::Brush; 21 | pub use renderer::RenderCommand; 22 | pub use renderer::RenderList; 23 | pub use renderer_type::RendererType; 24 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod borders; 2 | pub(crate) mod corner; 3 | pub(crate) mod cornerside; 4 | mod element_box; 5 | mod point; 6 | mod rectangle; 7 | pub(crate) mod side; 8 | mod size; 9 | mod trblrectangle; 10 | 11 | pub use element_box::ElementBox; 12 | pub use point::Point; 13 | pub use point::PointConverter; 14 | pub use rectangle::Rectangle; 15 | pub use size::Size; 16 | pub use trblrectangle::TrblRectangle; 17 | pub use side::Side; 18 | 19 | pub type Border = TrblRectangle; 20 | pub type Padding = TrblRectangle; 21 | pub type Margin = TrblRectangle; 22 | 23 | pub use kurbo::Affine; -------------------------------------------------------------------------------- /crates/craft_core/src/accessibility/access_handler.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | use craft_runtime::CraftRuntimeHandle; 3 | use crate::events::internal::InternalMessage; 4 | use accesskit::{ActionHandler, ActionRequest}; 5 | use craft_runtime::Sender; 6 | 7 | pub(crate) struct CraftAccessHandler { 8 | #[cfg(not(target_arch = "wasm32"))] 9 | #[allow(dead_code)] 10 | pub(crate) runtime_handle: CraftRuntimeHandle, 11 | #[allow(dead_code)] 12 | pub(crate) app_sender: Sender, 13 | } 14 | 15 | impl ActionHandler for CraftAccessHandler { 16 | fn do_action(&mut self, _request: ActionRequest) {} 17 | } 18 | -------------------------------------------------------------------------------- /examples/request/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "request" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "ani_list" 8 | path = "main.rs" 9 | 10 | [dependencies] 11 | 12 | util = { path = "../util" } 13 | 14 | tracing = { workspace = true } 15 | serde = { version = "1.0.213", features = ["derive"] } 16 | serde_json = "1.0.133" 17 | 18 | [dependencies.craft] 19 | path = "../../crates/craft" 20 | default-features = false 21 | features = ["vello_renderer", "devtools", "http_client", "png", "jpeg", "accesskit", "system_fonts"] 22 | package = "craft_gui" 23 | 24 | [dependencies.reqwest] 25 | workspace = true 26 | features = ["rustls-tls", "json"] -------------------------------------------------------------------------------- /website/src/docs/markdown/how_to_contribute.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | We welcome all contributions! If you're looking for a place to start, check out the `#todo` channel on our Discord. 3 | It contains a list of features we’re planning to add to Craft. 4 | 5 | ### New Features 6 | If you wish to propose a new feature, please create an [issue on GitHub](https://github.com/craft-gui/craft/issues/new) or just chat with us on Discord! 7 | 8 | ### Bugs 9 | Please report any bugs that you find on [GitHub](https://github.com/craft-gui/craft/issues/new). 10 | 11 | ### License 12 | Craft uses the [Unlicense](https://unlicense.org/) license, so any contribution will use that same license. -------------------------------------------------------------------------------- /crates/craft_core/src/events/update_queue_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::components::component::UpdateFn; 2 | use crate::components::ComponentId; 3 | use crate::components::{Event, Props}; 4 | use crate::PinnedFutureAny; 5 | 6 | pub struct UpdateQueueEntry { 7 | pub source_component: ComponentId, 8 | pub update_function: UpdateFn, 9 | pub update_result: Option, 10 | pub props: Props, 11 | } 12 | 13 | impl UpdateQueueEntry { 14 | pub fn new(source_component: u64, update_function: UpdateFn, update_result: Event, props: Props) -> Self { 15 | UpdateQueueEntry { 16 | source_component, 17 | update_function, 18 | update_result: update_result.future, 19 | props, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/craft_runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_runtime" 3 | version = "0.1.1" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | 8 | [target.'cfg(target_arch = "wasm32")'.dependencies.tokio] 9 | workspace = true 10 | default-features = false 11 | features = ["sync", "time"] 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.open] 14 | version = "5.3.2" 15 | default-features = false 16 | optional = true 17 | features = [] 18 | 19 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] 20 | workspace = true 21 | default-features = false 22 | features = ["rt-multi-thread", "sync", "time"] 23 | 24 | [target.'cfg(target_arch = "wasm32")'.dependencies] 25 | wasm-bindgen-futures = "0.4.50" 26 | 27 | [dependencies.cfg-if] 28 | workspace = true -------------------------------------------------------------------------------- /crates/craft_core/src/utils/cloneable_any.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | pub trait CloneableAny: Any { 4 | fn clone_box(&self) -> Box; 5 | 6 | fn as_any(&self) -> &dyn Any; 7 | fn as_any_mut(&mut self) -> &mut dyn Any; 8 | fn into_any(self: Box) -> Box; 9 | } 10 | 11 | impl CloneableAny for T 12 | where 13 | T: Any + Clone + 'static, // <-- ensure it's 'static 14 | { 15 | fn clone_box(&self) -> Box { 16 | Box::new(self.clone()) 17 | } 18 | 19 | fn as_any(&self) -> &dyn Any { 20 | self 21 | } 22 | 23 | fn as_any_mut(&mut self) -> &mut dyn Any { 24 | self 25 | } 26 | 27 | fn into_any(self: Box) -> Box { 28 | self 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/state_store.rs: -------------------------------------------------------------------------------- 1 | use crate::components::ComponentId; 2 | use std::any::Any; 3 | use std::collections::{HashMap, HashSet}; 4 | 5 | pub type StateStoreItem = dyn Any + Send; 6 | 7 | #[derive(Default)] 8 | pub struct StateStore { 9 | pub storage: HashMap>, 10 | } 11 | 12 | impl StateStore { 13 | pub(crate) fn remove_unused_state( 14 | &mut self, 15 | old_component_ids: &HashSet, 16 | new_component_ids: &HashSet, 17 | ) { 18 | // Get the old component ids that aren't in new_component_ids. 19 | old_component_ids.difference(new_component_ids).for_each(|component_id| { 20 | self.storage.remove(component_id); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/side.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::corner::Corner; 2 | 3 | #[repr(usize)] 4 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 5 | pub enum Side { 6 | Top = 0, 7 | Right = 1, 8 | Bottom = 2, 9 | Left = 3, 10 | } 11 | 12 | impl Side { 13 | pub(crate) fn next_clockwise(self) -> Self { 14 | // top -> right 15 | // right -> bottom 16 | // bottom -> left 17 | // left -> top 18 | unsafe { std::mem::transmute(((self as usize) + 1) & 3) } 19 | } 20 | 21 | pub(crate) fn as_corner(self) -> Corner { 22 | // top -> top_left 23 | // right -> top_right 24 | // bottom -> bottom_right 25 | // left -> bottom_left 26 | unsafe { std::mem::transmute(self) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/craft_core/src/components/props.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::sync::Arc; 3 | 4 | /// A read-only block of data that can be shared between threads. 5 | /// 6 | /// The `Props` struct allows for storing any type of data that implements 7 | /// `Any`, `Send`, and `Sync`. The data is stored in an `Arc`, making it 8 | /// safe for shared read-only access across multiple threads. 9 | #[derive(Clone, Debug)] 10 | pub struct Props { 11 | pub data: Arc, 12 | } 13 | 14 | impl Props { 15 | pub fn get_data(&self) -> Option<&T> { 16 | self.data.downcast_ref::() 17 | } 18 | 19 | pub fn new(data: T) -> Self 20 | where 21 | T: Any + Send + Sync, 22 | { 23 | Self { 24 | data: Arc::new(data), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /website/src/docs/installation.rs: -------------------------------------------------------------------------------- 1 | use crate::docs::docs_template; 2 | use crate::docs::markdown_viewer::{MarkdownViewer, MarkdownViewerProps}; 3 | use crate::WebsiteGlobalState; 4 | use craft::components::{Component, ComponentSpecification, Context, Props}; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct InstallationPage { 8 | } 9 | 10 | impl Component for InstallationPage { 11 | type GlobalState = WebsiteGlobalState; 12 | type Props = (); 13 | type Message = (); 14 | 15 | fn view(_context: &mut Context) -> ComponentSpecification { 16 | docs_template() 17 | .push(MarkdownViewer::component().props(Props::new(MarkdownViewerProps { 18 | markdown_text: include_str!("markdown/installation.md").to_string() 19 | }))) 20 | .component() 21 | } 22 | } -------------------------------------------------------------------------------- /website/src/docs/how_to_contribute.rs: -------------------------------------------------------------------------------- 1 | use crate::docs::docs_template; 2 | use crate::docs::markdown_viewer::{MarkdownViewer, MarkdownViewerProps}; 3 | use crate::WebsiteGlobalState; 4 | use craft::components::{Component, ComponentSpecification, Context, Props}; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct HowToContributePage { 8 | 9 | } 10 | 11 | impl Component for HowToContributePage { 12 | type GlobalState = WebsiteGlobalState; 13 | type Props = (); 14 | type Message = (); 15 | 16 | fn view(_context: &mut Context) -> ComponentSpecification { 17 | docs_template() 18 | .push(MarkdownViewer::component().props(Props::new(MarkdownViewerProps { 19 | markdown_text: include_str!("markdown/how_to_contribute.md").to_string() 20 | }))) 21 | .component() 22 | } 23 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/tinyvg_resource.rs: -------------------------------------------------------------------------------- 1 | use crate::resource_data::ResourceData; 2 | use tinyvg_rs::TinyVg; 3 | 4 | #[derive(Debug)] 5 | pub struct TinyVgResource { 6 | pub common_data: ResourceData, 7 | pub tinyvg: Option, 8 | } 9 | 10 | impl TinyVgResource { 11 | pub(crate) fn new(mut data: ResourceData) -> Self { 12 | if let Some(tinyvg_data) = data.data.as_ref() { 13 | let tinyvg = TinyVg::from_bytes(tinyvg_data); 14 | data.data = None; 15 | 16 | TinyVgResource { 17 | common_data: data, 18 | tinyvg: tinyvg.ok(), 19 | } 20 | } else { 21 | data.data = None; 22 | 23 | TinyVgResource { 24 | common_data: data, 25 | tinyvg: None, 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/craft_core/src/text/text_context.rs: -------------------------------------------------------------------------------- 1 | use parley::{FontContext, TextStyle, TreeBuilder}; 2 | use craft_primitives::ColorBrush; 3 | 4 | pub struct TextContext { 5 | pub font_context: FontContext, 6 | pub layout_context: parley::LayoutContext, 7 | } 8 | 9 | impl Default for TextContext { 10 | fn default() -> Self { 11 | Self::new() 12 | } 13 | } 14 | 15 | impl TextContext { 16 | pub fn new() -> Self { 17 | Self { 18 | font_context: Default::default(), 19 | layout_context: Default::default(), 20 | } 21 | } 22 | 23 | pub fn tree_builder<'a>( 24 | &'a mut self, 25 | scale: f32, 26 | raw_style: &TextStyle<'_, ColorBrush>, 27 | ) -> TreeBuilder<'a, ColorBrush> { 28 | self.layout_context.tree_builder(&mut self.font_context, scale, true, raw_style) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /website/src/docs/styling.rs: -------------------------------------------------------------------------------- 1 | use crate::WebsiteGlobalState; 2 | use craft::components::{Component, ComponentSpecification, Context}; 3 | use craft::elements::{Container, ElementStyles, Text}; 4 | use craft::style::{Display, FlexDirection, Weight}; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct StylingPage { 8 | 9 | } 10 | 11 | impl Component for StylingPage { 12 | type GlobalState = WebsiteGlobalState; 13 | type Props = (); 14 | type Message = (); 15 | 16 | fn view(_context: &mut Context) -> ComponentSpecification { 17 | Container::new() 18 | .display(Display::Flex) 19 | .flex_direction(FlexDirection::Column) 20 | .push(Text::new("Styling").font_size(32.0).margin("0px", "0px", "25px", "0px").font_weight(Weight::BOLD)) 21 | .push(Text::new("Coming Soon!").font_size(16.0)) 22 | .component() 23 | } 24 | } -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /website/src/link.rs: -------------------------------------------------------------------------------- 1 | use crate::WebsiteGlobalState; 2 | use craft::components::{Component, ComponentSpecification, Context}; 3 | use craft::elements::Text; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct Link; 7 | 8 | #[derive(Default)] 9 | pub(crate) struct LinkProps { 10 | pub(crate) href: String, 11 | } 12 | 13 | impl Component for Link { 14 | type GlobalState = WebsiteGlobalState; 15 | type Props = LinkProps; 16 | type Message = (); 17 | 18 | fn view(context: &mut Context) -> ComponentSpecification { 19 | context.children().first().unwrap_or(&Text::new("Invalid Link").component()).clone() 20 | } 21 | 22 | fn update(context: &mut Context) { 23 | if context.message().clicked() { 24 | let href = context.props().href.clone(); 25 | context.global_state_mut().set_route(href.as_str()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /website/src/docs/hello_world.rs: -------------------------------------------------------------------------------- 1 | use crate::docs::docs_template; 2 | use crate::examples::counter::Counter; 3 | use crate::docs::markdown_viewer::{MarkdownViewer, MarkdownViewerProps}; 4 | use crate::WebsiteGlobalState; 5 | use craft::components::{Component, ComponentSpecification, Context, Props}; 6 | 7 | #[derive(Default)] 8 | pub(crate) struct HelloWorldPage { 9 | 10 | } 11 | 12 | impl Component for HelloWorldPage { 13 | type GlobalState = WebsiteGlobalState; 14 | type Props = (); 15 | type Message = (); 16 | 17 | fn view(_context: &mut Context) -> ComponentSpecification { 18 | docs_template() 19 | .push(MarkdownViewer::component().props(Props::new(MarkdownViewerProps { 20 | markdown_text: include_str!("markdown/hello_world/intro.md").to_string() 21 | }))) 22 | .push(Counter::component()) 23 | .component() 24 | } 25 | } -------------------------------------------------------------------------------- /crates/craft_core/src/text/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod text_context; 2 | pub(crate) mod text_render_data; 3 | pub(crate) mod parley_editor; 4 | 5 | use std::ops::Range; 6 | pub use parley; 7 | 8 | use crate::style::{Style, TextStyleProperty}; 9 | pub use text_render_data::from_editor; 10 | 11 | #[derive(PartialEq)] 12 | pub(crate) struct TextStyle { 13 | pub(crate) font_size: f32, 14 | } 15 | 16 | impl From<&Style> for TextStyle { 17 | fn from(style: &Style) -> Self { 18 | TextStyle { 19 | font_size: style.font_size(), 20 | } 21 | } 22 | } 23 | 24 | #[derive(Clone)] 25 | #[derive(Default)] 26 | #[derive(PartialEq)] 27 | pub struct RangedStyles { 28 | pub styles: Vec<(Range, TextStyleProperty)>, 29 | } 30 | 31 | impl RangedStyles { 32 | pub fn new(styles: Vec<(Range, TextStyleProperty)>) -> Self { 33 | Self { 34 | styles 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /website/src/docs/state_management.rs: -------------------------------------------------------------------------------- 1 | use crate::WebsiteGlobalState; 2 | use craft::components::{Component, ComponentSpecification, Context}; 3 | use craft::elements::{Container, ElementStyles, Text}; 4 | use craft::style::{Display, FlexDirection, Weight}; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct StateManagementPage { 8 | 9 | } 10 | 11 | impl Component for StateManagementPage { 12 | type GlobalState = WebsiteGlobalState; 13 | type Props = (); 14 | type Message = (); 15 | 16 | fn view(_context: &mut Context) -> ComponentSpecification { 17 | Container::new() 18 | .display(Display::Flex) 19 | .flex_direction(FlexDirection::Column) 20 | .push(Text::new("State Management").font_size(32.0).margin("0px", "0px", "25px", "0px").font_weight(Weight::BOLD)) 21 | .push(Text::new("Coming Soon!").font_size(16.0)) 22 | .component() 23 | } 24 | } -------------------------------------------------------------------------------- /crates/craft_resource_manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_resource_manager" 3 | version = "0.1.1" 4 | edition.workspace = true 5 | 6 | [features] 7 | http_client = ["dep:reqwest"] 8 | 9 | [dependencies.craft_logging] 10 | path = "../craft_logger" 11 | default-features = false 12 | version = "0.1.0" 13 | 14 | [dependencies.craft_runtime] 15 | path = "../craft_runtime" 16 | default-features = false 17 | version = "0.1.1" 18 | 19 | [target.'cfg(not(target_os = "android"))'.dependencies.reqwest] 20 | workspace = true 21 | default-features = false 22 | features = ["native-tls"] 23 | optional = true 24 | 25 | [target.'cfg(target_os = "android")'.dependencies.reqwest] 26 | workspace = true 27 | default-features = false 28 | features = ["rustls-tls"] 29 | optional = true 30 | 31 | [dependencies.image] 32 | workspace = true 33 | 34 | [dependencies.tinyvg-rs] 35 | workspace = true 36 | 37 | [dependencies.chrono] 38 | workspace = true 39 | -------------------------------------------------------------------------------- /crates/craft_core/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod component; 2 | mod props; 3 | mod update_result; 4 | 5 | #[cfg(feature = "code_highlighting")] 6 | mod code_editor; 7 | #[cfg(feature = "link")] 8 | mod web_link; 9 | 10 | pub use crate::events::UserMessage; 11 | pub use component::Component; 12 | pub use component::Context; 13 | pub use component::ComponentId; 14 | pub use component::ComponentOrElement; 15 | pub use component::ComponentSpecification; 16 | pub use props::Props; 17 | pub use update_result::Event; 18 | pub use update_result::ImeAction; 19 | pub use update_result::FocusAction; 20 | pub use update_result::PointerCapture; 21 | 22 | #[cfg(feature = "code_highlighting")] 23 | pub use { 24 | code_editor::CodeEditor, 25 | code_editor::CodeEditorProps, 26 | code_editor::syntect, 27 | }; 28 | 29 | 30 | #[cfg(feature = "link")] 31 | pub use { 32 | web_link::WebLink, 33 | web_link::WebLinkProps, 34 | web_link::open, 35 | }; -------------------------------------------------------------------------------- /website/src/docs/markdown/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | This tutorial currently targets the main branch. Create a new Rust project using Cargo: 4 | 5 | ```bash 6 | cargo new --bin hello_craft 7 | ``` 8 | Next, add the `craft` crate to your `Cargo.toml` file: 9 | 10 | ```toml 11 | [dependencies.craft] 12 | git = "https://github.com/craft-gui/craft.git" 13 | branch = "main" 14 | features = ["vello_renderer", "devtools", "accesskit", "system_fonts"] 15 | package = "craft_gui" 16 | ``` 17 | 18 | *** 19 | 20 | ## Next Steps 21 | 1. Create a counter app. 22 | 2. Learn the Elm architecture. 23 | * View 24 | * Update 25 | * Async Updates 26 | 3. Learn about the craft. 27 | * Widgets 28 | * Layout 29 | * Styling 30 | 31 | [Google.com](https://www.google.com/search?q=craft+gui+rust) is a good place to start. 32 | 33 | ![A mushroom-head robot drinking bubble tea](https://raw.githubusercontent.com/Codecademy/docs/main/media/codey.jpg 'Codey, the Codecademy mascot, drinking bubble tea') -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/resource_data.rs: -------------------------------------------------------------------------------- 1 | use crate::identifier::ResourceIdentifier; 2 | use crate::resource_type::ResourceType; 3 | use chrono::{DateTime, Utc}; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Debug)] 7 | pub struct ResourceData { 8 | pub(crate) resource_identifier: ResourceIdentifier, 9 | pub(crate) data: Option>, 10 | pub(crate) resource_type: ResourceType, 11 | pub(crate) expiration_time: Option>, 12 | } 13 | 14 | impl ResourceData { 15 | pub(crate) fn new( 16 | resource_identifier: ResourceIdentifier, 17 | data: Option>, 18 | expiration_time: Option>, 19 | resource_type: ResourceType, 20 | ) -> Self { 21 | ResourceData { 22 | resource_identifier, 23 | expiration_time, 24 | data, 25 | resource_type, 26 | } 27 | } 28 | 29 | pub fn expiration_time(&self) -> Option> { 30 | self.expiration_time 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::image::ImageResource; 2 | use crate::tinyvg_resource::TinyVgResource; 3 | use std::sync::Arc; 4 | use image::EncodableLayout; 5 | use crate::resource_data::ResourceData; 6 | 7 | #[derive(Debug)] 8 | pub enum Resource { 9 | Image(Arc), 10 | Font(ResourceData), 11 | TinyVg(TinyVgResource), 12 | } 13 | 14 | impl Resource { 15 | pub fn data(&self) -> Option<&[u8]> { 16 | match self { 17 | Resource::Image(data) => data.common_data.data.as_deref(), 18 | Resource::Font(common_data) => common_data.data.as_ref().map(|d| d.as_bytes()), 19 | Resource::TinyVg(data) => data.common_data.data.as_deref(), 20 | } 21 | } 22 | 23 | pub fn common_data(&self) -> &ResourceData { 24 | match self { 25 | Resource::Image(data) => &data.common_data, 26 | Resource::Font(data) => data, 27 | Resource::TinyVg(data) => &data.common_data, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/stateful_element.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::base_element_state::BaseElementState; 2 | use crate::elements::Element; 3 | use crate::reactive::element_state_store::ElementStateStore; 4 | 5 | pub trait StatefulElement : Element { 6 | 7 | fn state<'a>(&self, element_state: &'a ElementStateStore) -> &'a State { 8 | element_state.storage.get(&self.element_data().component_id).unwrap().data.as_ref().downcast_ref().unwrap() 9 | } 10 | 11 | fn state_mut<'a>(&self, element_state: &'a mut ElementStateStore) -> &'a mut State { 12 | element_state.storage.get_mut(&self.element_data().component_id).unwrap().data.as_mut().downcast_mut().unwrap() 13 | } 14 | 15 | fn state_and_base_mut<'a>(&self, element_state: &'a mut ElementStateStore) -> (&'a mut State, &'a mut BaseElementState) { 16 | let state = element_state.storage.get_mut(&self.element_data().component_id).unwrap(); 17 | (state.data.as_mut().downcast_mut().unwrap(), &mut state.base) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /crates/craft_core/src/components/web_link.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{Component, ComponentSpecification, Context}; 2 | use crate::elements::Text; 3 | 4 | #[derive(Default)] 5 | pub struct WebLink; 6 | 7 | #[derive(Default)] 8 | pub struct WebLinkProps { 9 | pub(crate) href: String, 10 | } 11 | 12 | impl Component for WebLink { 13 | type GlobalState = (); 14 | type Props = WebLinkProps; 15 | type Message = (); 16 | 17 | fn view(context: &mut Context) -> ComponentSpecification { 18 | context.children().first().unwrap_or(&Text::new("Invalid Link").component()).clone() 19 | } 20 | 21 | fn update(context: &mut Context) { 22 | if context.message().clicked() { 23 | open(context.props().href.as_str()) 24 | } 25 | } 26 | } 27 | 28 | pub fn open(link: &str) { 29 | #[cfg(target_arch = "wasm32")] 30 | { 31 | if let Some(win) = web_sys::window() { 32 | let _ = win.open_with_url(link); 33 | } 34 | } 35 | 36 | #[cfg(not(target_arch = "wasm32"))] 37 | { 38 | open::that(link).unwrap(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/craft_core/src/events/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::components::component::UpdateFn; 2 | use crate::components::ComponentId; 3 | use crate::components::Props; 4 | use craft_resource_manager::resource_event::ResourceEvent; 5 | 6 | use crate::events::CloneableAny; 7 | #[cfg(target_arch = "wasm32")] 8 | use {craft_renderer::renderer::Renderer, std::sync::Arc, winit::window::Window}; 9 | 10 | pub struct InternalUserMessage { 11 | pub update_fn: UpdateFn, 12 | pub source_component_id: ComponentId, 13 | #[cfg(not(target_arch = "wasm32"))] 14 | pub message: Box, 15 | #[cfg(target_arch = "wasm32")] 16 | pub message: Box, 17 | pub props: Props, 18 | } 19 | 20 | pub enum InternalMessage { 21 | GotUserMessage(InternalUserMessage), 22 | ResourceEvent(ResourceEvent), 23 | #[cfg(target_arch = "wasm32")] 24 | RendererCreated(Arc, Box), 25 | } 26 | 27 | impl From for InternalMessage { 28 | fn from(event: ResourceEvent) -> Self { 29 | InternalMessage::ResourceEvent(event) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/image.rs: -------------------------------------------------------------------------------- 1 | use crate::resource_data::ResourceData; 2 | use image::{EncodableLayout, RgbaImage}; 3 | 4 | #[derive(Debug)] 5 | pub struct ImageResource { 6 | pub common_data: ResourceData, 7 | pub width: u32, 8 | pub height: u32, 9 | pub image: RgbaImage, 10 | } 11 | 12 | impl ImageResource { 13 | pub(crate) fn new(width: u32, height: u32, mut data: ResourceData) -> Self { 14 | if let Some(image_data) = data.data.take() { 15 | let image = image::load_from_memory(image_data.as_bytes()).unwrap(); 16 | let image = image.to_rgba8(); 17 | 18 | ImageResource { 19 | common_data: data, 20 | image, 21 | width, 22 | height, 23 | } 24 | } else { 25 | let empty_image = RgbaImage::new(0, 0); 26 | data.data = None; 27 | 28 | ImageResource { 29 | common_data: data, 30 | image: empty_image, 31 | width, 32 | height, 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/craft_renderer/src/blank_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use craft_primitives::geometry::Rectangle; 3 | use craft_primitives::Color; 4 | use crate::renderer::{RenderList, Renderer}; 5 | use craft_resource_manager::ResourceManager; 6 | use std::sync::Arc; 7 | use crate::text_renderer_data::TextRender; 8 | 9 | pub struct BlankRenderer; 10 | 11 | impl Renderer for BlankRenderer { 12 | fn surface_width(&self) -> f32 { 13 | 0.0 14 | } 15 | 16 | fn surface_height(&self) -> f32 { 17 | 0.0 18 | } 19 | 20 | fn resize_surface(&mut self, _width: f32, _height: f32) {} 21 | 22 | fn surface_set_clear_color(&mut self, _color: Color) {} 23 | fn as_any_mut(&mut self) -> &mut dyn Any { 24 | self 25 | } 26 | 27 | fn prepare_render_list<'a>( 28 | &mut self, 29 | _render_list: &mut RenderList, 30 | _resource_manager: Arc, 31 | _window: Rectangle, 32 | _get_text_renderer: Box Option<&'a TextRender> + 'a>, 33 | ) { 34 | } 35 | 36 | fn submit(&mut self, _resource_manager: Arc) {} 37 | } 38 | -------------------------------------------------------------------------------- /website/src/web_link.rs: -------------------------------------------------------------------------------- 1 | use crate::WebsiteGlobalState; 2 | use craft::components::{Component, ComponentSpecification, Context}; 3 | use craft::elements::Text; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct WebLink; 7 | 8 | #[derive(Default)] 9 | pub(crate) struct WebLinkProps { 10 | pub(crate) href: String, 11 | } 12 | 13 | impl Component for WebLink { 14 | type GlobalState = WebsiteGlobalState; 15 | type Props = WebLinkProps; 16 | type Message = (); 17 | 18 | fn view(context: &mut Context) -> ComponentSpecification { 19 | context.children().first().unwrap_or(&Text::new("Invalid Link").component()).clone() 20 | } 21 | 22 | fn update(context: &mut Context) { 23 | if context.message().clicked() { 24 | #[cfg(target_arch = "wasm32")] 25 | { 26 | if let Some(win) = web_sys::window() { 27 | let _ = win.open_with_url(context.props().href.as_str()); 28 | } 29 | } 30 | 31 | #[cfg(not(target_arch = "wasm32"))] 32 | { 33 | open::that(context.props().href.as_str()).unwrap(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/size.rs: -------------------------------------------------------------------------------- 1 | /// A structure representing the size of a 2D object. 2 | #[derive(Copy, Clone, Debug, Default)] 3 | pub struct Size { 4 | /// The width of the object. 5 | pub width: T, 6 | /// The height of the object. 7 | pub height: T, 8 | } 9 | 10 | impl Size { 11 | /// Creates a new `Size` with the given width and height. 12 | /// 13 | /// # Arguments 14 | /// 15 | /// * `width` - A float representing the width of the object. 16 | /// * `height` - A float representing the height of the object. 17 | /// 18 | /// # Returns 19 | /// 20 | /// A `Size` instance with the specified width and height. 21 | pub fn new(width: T, height: T) -> Self { 22 | Self { width, height } 23 | } 24 | } 25 | 26 | /*impl From> for Size { 27 | /// Converts a `taffy::Size` to a `Size`. 28 | /// 29 | /// # Arguments 30 | /// 31 | /// * `size` - A `taffy::Size` instance to convert. 32 | /// 33 | /// # Returns 34 | /// 35 | /// A `Size` instance with the same width and height as the input. 36 | fn from(size: taffy::Size) -> Self { 37 | Self::new(size.width, size.height) 38 | } 39 | } 40 | */ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /crates/craft/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /crates/craft_core/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /crates/craft_dylib/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/trblrectangle.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, Default)] 2 | pub struct TrblRectangle 3 | where 4 | T: Copy, 5 | { 6 | pub top: T, 7 | pub right: T, 8 | pub bottom: T, 9 | pub left: T, 10 | } 11 | 12 | impl TrblRectangle 13 | where 14 | T: Copy, 15 | { 16 | pub const fn new(top: T, right: T, bottom: T, left: T) -> Self { 17 | Self { 18 | top, 19 | right, 20 | bottom, 21 | left, 22 | } 23 | } 24 | pub const fn new_all(value: T) -> Self { 25 | Self { 26 | top: value, 27 | right: value, 28 | bottom: value, 29 | left: value, 30 | } 31 | } 32 | 33 | #[allow(dead_code)] 34 | pub const fn to_array(self) -> [T; 4] { 35 | [self.top, self.right, self.bottom, self.left] 36 | } 37 | } 38 | 39 | /*impl From> for TrblRectangle { 40 | fn from(rect: taffy::Rect) -> Self { 41 | TrblRectangle::new(rect.top, rect.right, rect.bottom, rect.left) 42 | } 43 | } 44 | 45 | impl From> for TrblRectangle { 46 | fn from(rect: taffy::Rect) -> Self { 47 | TrblRectangle::new(rect.top, rect.right, rect.bottom, rect.left) 48 | } 49 | } 50 | */ -------------------------------------------------------------------------------- /crates/craft_logger/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /crates/craft_core/src/options.rs: -------------------------------------------------------------------------------- 1 | use craft_primitives::geometry::Size; 2 | use craft_renderer::RendererType; 3 | 4 | /// Configuration options for the Craft application. 5 | /// 6 | /// This struct holds various options that can be used to customize the behavior 7 | /// of the application. In particular, it configures which renderer to use and 8 | /// sets the default window title. 9 | pub struct CraftOptions { 10 | /// The type of renderer to use. 11 | /// 12 | /// The renderer is chosen based on the features enabled at compile time. 13 | /// See [`RendererType`] for details. 14 | pub renderer: RendererType, 15 | /// The title of the application window. 16 | /// 17 | /// Defaults to `"craft"`. 18 | pub window_title: String, 19 | /// The initial size of the window. 20 | pub window_size: Option>, 21 | } 22 | 23 | impl Default for CraftOptions { 24 | fn default() -> Self { 25 | Self { 26 | renderer: RendererType::default(), 27 | window_title: "craft".to_string(), 28 | window_size: None, 29 | } 30 | } 31 | } 32 | 33 | impl CraftOptions { 34 | pub fn basic(title: &str) -> Self { 35 | Self { 36 | renderer: RendererType::default(), 37 | window_title: title.to_string(), 38 | window_size: None, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/craft_renderer/src/text_renderer_data.rs: -------------------------------------------------------------------------------- 1 | use craft_primitives::geometry::Rectangle; 2 | use peniko::Color; 3 | use peniko::kurbo::{Affine, Line}; 4 | use craft_primitives::ColorBrush; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct TextRender { 8 | pub lines: Vec, 9 | pub cursor: Option<(Rectangle, Color)>, 10 | pub override_brush: Option, 11 | } 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct TextRenderLine { 15 | pub items: Vec, 16 | pub selections: Vec<(Rectangle, Color)>, 17 | pub backgrounds: Vec<(Rectangle, Color)>, 18 | } 19 | 20 | #[derive(Clone, Debug)] 21 | pub struct TextRenderItem { 22 | pub brush: ColorBrush, 23 | #[allow(dead_code)] 24 | pub underline: Option, 25 | #[allow(dead_code)] 26 | pub strikethrough: Option, 27 | #[allow(dead_code)] 28 | pub glyph_transform: Option, 29 | pub font_size: f32, 30 | pub glyphs: Vec, 31 | pub font: peniko::Font, 32 | } 33 | 34 | #[derive(Clone, Copy, Debug)] 35 | pub struct TextRenderItemLine { 36 | pub brush: ColorBrush, 37 | #[allow(dead_code)] 38 | pub line: Line, 39 | #[allow(dead_code)] 40 | pub width: f32, 41 | } 42 | 43 | #[derive(Clone, Copy, Debug)] 44 | pub struct TextRenderGlyph { 45 | pub id: u16, 46 | pub x: f32, 47 | pub y: f32, 48 | } -------------------------------------------------------------------------------- /website/src/theme.rs: -------------------------------------------------------------------------------- 1 | use craft::elements::{Container, ElementStyles}; 2 | use craft::style::Unit; 3 | use craft::Color; 4 | 5 | pub(crate) const BODY_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); 6 | 7 | pub(crate) const NAVBAR_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); 8 | pub(crate) const NAVBAR_TEXT_COLOR: Color = Color::from_rgb8(50, 50, 50); 9 | pub(crate) const NAVBAR_TEXT_HOVERED_COLOR: Color = Color::from_rgb8(0, 0, 0); 10 | 11 | pub(crate) const ACTIVE_LINK_COLOR: Color = Color::from_rgb8(42, 108, 200); 12 | pub(crate) const DEFAULT_LINK_COLOR: Color = Color::from_rgb8(102, 102, 102); 13 | 14 | 15 | pub(crate) const WRAPPER_MAX_WIDTH: Unit = Unit::Px(1300.0); 16 | pub(crate) const WRAPPER_MARGIN_LEFT: Unit = Unit::Auto; 17 | pub(crate) const WRAPPER_MARGIN_RIGHT: Unit = Unit::Auto; 18 | pub(crate) const WRAPPER_PADDING_LEFT: Unit = Unit::Px(20.0); 19 | pub(crate) const WRAPPER_PADDING_RIGHT: Unit = Unit::Px(20.0); 20 | 21 | 22 | pub(crate) const MOBILE_MEDIA_QUERY_WIDTH: f32 = 850.0; 23 | pub(crate) const MAX_DOCS_CONTENT_WIDTH: f32 = 750.0; 24 | 25 | pub(crate) fn wrapper() -> Container { 26 | Container::new() 27 | .margin(Unit::Px(0.0), WRAPPER_MARGIN_RIGHT, Unit::Px(0.0), WRAPPER_MARGIN_LEFT) 28 | .padding(Unit::Px(0.0), WRAPPER_PADDING_RIGHT, Unit::Px(0.0), WRAPPER_PADDING_LEFT) 29 | .width("100%") 30 | .max_width(WRAPPER_MAX_WIDTH) 31 | } -------------------------------------------------------------------------------- /examples/util/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub struct ExampleProps { 2 | pub show_scrollbar: bool, 3 | } 4 | 5 | impl Default for ExampleProps { 6 | fn default() -> Self { 7 | ExampleProps { show_scrollbar: true } 8 | } 9 | } 10 | 11 | #[allow(dead_code)] 12 | pub fn setup_logging() { 13 | #[cfg(target_arch = "wasm32")] 14 | { 15 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 16 | 17 | use tracing::level_filters::LevelFilter; 18 | use tracing_subscriber::fmt::format::Pretty; 19 | use tracing_subscriber::layer::SubscriberExt; 20 | use tracing_subscriber::util::SubscriberInitExt; 21 | use tracing_subscriber::Layer; 22 | use tracing_web::{performance_layer, MakeWebConsoleWriter}; 23 | 24 | let fmt_layer = tracing_subscriber::fmt::layer() 25 | .with_ansi(true) 26 | .without_time() 27 | .with_writer(MakeWebConsoleWriter::new()) 28 | .with_filter(LevelFilter::INFO); 29 | 30 | let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); 31 | 32 | tracing_subscriber::registry().with(fmt_layer).with(perf_layer).init(); 33 | } 34 | #[cfg(not(target_arch = "wasm32"))] 35 | { 36 | use tracing_subscriber::fmt::format::FmtSpan; 37 | tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).with_span_events(FmtSpan::CLOSE).init(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "website" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | 8 | tracing-subscriber = "0.3.19" 9 | tracing = "0.1.41" 10 | 11 | util = { path = "../examples/util" } 12 | 13 | serde = { version = "1.0.213", features = ["derive"] } 14 | serde_json = "1.0.133" 15 | web-sys = { version = "0.3.77", features = ["Window", "Location", "History"] } 16 | 17 | [target.'cfg(target_arch = "wasm32")'.dependencies.craft] 18 | path = "../crates/craft" 19 | default-features = false 20 | features = [ 21 | "vello_hybrid_renderer", 22 | "vello_hybrid_renderer_webgl", 23 | "devtools", 24 | "http_client", 25 | "png", 26 | "jpeg", 27 | "accesskit", 28 | "system_fonts", 29 | "markdown" 30 | ] 31 | package = "craft_gui" 32 | 33 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.craft] 34 | path = "../crates/craft" 35 | default-features = false 36 | features = [ 37 | "vello_renderer", 38 | "devtools", 39 | "http_client", 40 | "png", 41 | "jpeg", 42 | "accesskit", 43 | "system_fonts", 44 | "markdown" 45 | ] 46 | package = "craft_gui" 47 | 48 | [target.'cfg(target_arch = "wasm32")'.dependencies] 49 | tracing-web = "0.1.3" 50 | console_error_panic_hook = "0.1.7" 51 | 52 | [dependencies.reqwest] 53 | workspace = true 54 | default-features = false 55 | features = ["rustls-tls", "json"] 56 | 57 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 58 | open = "5" -------------------------------------------------------------------------------- /crates/craft_core/src/wasm_queue.rs: -------------------------------------------------------------------------------- 1 | use crate::events::internal::InternalMessage; 2 | use std::cell::RefCell; 3 | 4 | pub const WASM_QUEUE_SIZE: usize = 100; 5 | 6 | pub struct WasmQueue { 7 | queue: [Option; WASM_QUEUE_SIZE], 8 | next: usize, 9 | } 10 | 11 | impl Default for WasmQueue { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | impl WasmQueue { 18 | pub const fn new() -> Self { 19 | Self { 20 | queue: [const { None }; WASM_QUEUE_SIZE], 21 | next: 0, 22 | } 23 | } 24 | 25 | /// Push, overwriting the oldest entry if we’re full (ring buffer). 26 | pub fn push(&mut self, msg: InternalMessage) { 27 | self.queue[self.next] = Some(msg); 28 | self.next = (self.next + 1) % WASM_QUEUE_SIZE; 29 | } 30 | 31 | /// Drain all pending messages, calling `f` for each. 32 | pub fn drain(&mut self, mut f: F) { 33 | for slot in self.queue.iter_mut() { 34 | if let Some(msg) = slot.take() { 35 | f(msg); 36 | } else { 37 | break; 38 | } 39 | } 40 | self.next = 0; 41 | } 42 | 43 | pub fn len(&self) -> usize { 44 | self.next 45 | } 46 | 47 | pub fn is_empty(&self) -> bool { 48 | self.next == 0 49 | } 50 | } 51 | 52 | thread_local! { 53 | pub static WASM_QUEUE: RefCell = const { RefCell::new(WasmQueue::new()) }; 54 | } 55 | -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/element_state_store.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{ComponentId, FocusAction}; 2 | use crate::elements::base_element_state::BaseElementState; 3 | use std::any::Any; 4 | use std::collections::{HashMap, HashSet}; 5 | 6 | #[derive(Debug)] 7 | pub struct ElementStateStoreItem { 8 | pub base: BaseElementState, 9 | pub data: Box, 10 | } 11 | 12 | #[derive(Default)] 13 | pub struct ElementStateStore { 14 | pub storage: HashMap, 15 | } 16 | 17 | impl ElementStateStore { 18 | pub(crate) fn update_element_focus(&mut self, focus: FocusAction) { 19 | match focus { 20 | FocusAction::None => {} 21 | FocusAction::Set(id) => { 22 | for (element_id, value) in self.storage.iter_mut() { 23 | value.base.focused = *element_id == id; 24 | } 25 | } 26 | FocusAction::Unset => { 27 | for value in self.storage.values_mut() { 28 | value.base.focused = false; 29 | } 30 | } 31 | } 32 | } 33 | 34 | pub(crate) fn remove_unused_state( 35 | &mut self, 36 | old_element_ids: &HashSet, 37 | new_element_ids: &HashSet, 38 | ) { 39 | // Get the old element ids that aren't in new_element_ids. 40 | old_element_ids.difference(new_element_ids).for_each(|element_id| { 41 | self.storage.remove(element_id); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /website/src/router.rs: -------------------------------------------------------------------------------- 1 | use crate::docs::docs_component::Docs; 2 | use crate::examples::Examples; 3 | use crate::index::index_page; 4 | use craft::components::{Component, ComponentSpecification}; 5 | use craft::WindowContext; 6 | 7 | #[derive(Clone)] 8 | pub(crate) struct MappedPath<'a> { 9 | pub(crate) path: &'a str, 10 | pub(crate) component_specification: ComponentSpecification, 11 | } 12 | 13 | impl<'a> MappedPath<'a> { 14 | pub(crate) fn new(path: &'a str, component_specification: ComponentSpecification) -> Self { 15 | MappedPath { 16 | path, 17 | component_specification, 18 | } 19 | } 20 | } 21 | 22 | pub fn resolve_route<'a>(path: &'a str, window_ctx: &'a WindowContext) -> Option> { 23 | let mapped_paths: Vec = vec![ 24 | MappedPath::new("/examples/*", Examples::component().key("examples")), 25 | MappedPath::new("/docs/*", Docs::component().key("docs")), 26 | MappedPath::new("/*", index_page(window_ctx).key("index")), 27 | ]; 28 | for mapped_path in &mapped_paths { 29 | let mut matches = true; 30 | for (path_resource, rule_token) in path.split("/").zip(mapped_path.path.split("/")) { 31 | if rule_token == "*" { 32 | continue; 33 | } 34 | 35 | if rule_token != path_resource { 36 | matches = false; 37 | break; 38 | } 39 | } 40 | 41 | if matches { 42 | return Some(mapped_path.clone()); 43 | } 44 | } 45 | 46 | None 47 | } 48 | -------------------------------------------------------------------------------- /examples/custom_event_loop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_event_loop" 3 | version = "0.1.0" 4 | edition = "2024" 5 | default-run = "custom_event_loop" 6 | 7 | [[bin]] 8 | name = "custom_event_loop" 9 | path = "main.rs" 10 | 11 | [dependencies] 12 | cfg-if = "1.0.0" 13 | bytemuck = "1.23.0" 14 | glam = "0.30.4" 15 | 16 | [dependencies.wgpu] 17 | version = "24.0.3" 18 | default-features = false 19 | features = ["wgsl"] 20 | 21 | [dependencies.craft] 22 | path = "../../crates/craft" 23 | default-features = false 24 | features = ["vello_renderer", "devtools", "accesskit", "system_fonts"] 25 | package = "craft_gui" 26 | 27 | [dependencies.craft_renderer] 28 | path = "../../crates/craft_renderer" 29 | default-features = false 30 | features = ["vello_renderer"] 31 | 32 | [target.'cfg(not(target_os = "android"))'.dependencies.winit] 33 | version = "0.30.11" 34 | features = [] 35 | 36 | [target.'cfg(target_os = "android")'.dependencies.winit] 37 | version = "0.30.11" 38 | features = ["android-native-activity"] 39 | 40 | [target.'cfg(target_arch = "wasm32")'.dependencies] 41 | wasm-bindgen-futures = "0.4.50" 42 | 43 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 44 | version = "0.3.77" 45 | default-features = false 46 | features = [ 47 | "Document", 48 | "Window", 49 | "Element", 50 | ] 51 | 52 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-time] 53 | version = "1.1.0" 54 | default-features = false 55 | features = [] 56 | 57 | [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] 58 | version = "0.2.100" 59 | default-features = false 60 | features = ["std", "msrv"] -------------------------------------------------------------------------------- /website/src/docs/markdown_viewer.rs: -------------------------------------------------------------------------------- 1 | use crate::docs::docs_template; 2 | use crate::WebsiteGlobalState; 3 | use craft::components::{Component, ComponentSpecification, Context}; 4 | use craft::elements::Text; 5 | use craft::events::{CraftMessage, Message}; 6 | 7 | #[derive(Default)] 8 | pub(crate) struct MarkdownViewer { 9 | markdown: Option 10 | } 11 | 12 | #[derive(Default)] 13 | pub(crate) struct MarkdownViewerProps { 14 | pub(crate) markdown_text: String, 15 | } 16 | 17 | impl Component for MarkdownViewer { 18 | type GlobalState = WebsiteGlobalState; 19 | type Props = MarkdownViewerProps; 20 | type Message = (); 21 | 22 | fn view(context: &mut Context) -> ComponentSpecification { 23 | if let Some(markdown) = &context.state().markdown { 24 | docs_template() 25 | .push(markdown.clone()) 26 | .component() 27 | } else { 28 | docs_template() 29 | .push(Text::new("Loading...")) 30 | .component() 31 | } 32 | } 33 | 34 | fn update(context: &mut Context) { 35 | if let Message::CraftMessage(CraftMessage::LinkClicked(link)) = context.message() { 36 | craft::components::open(link); 37 | context.event_mut().prevent_propagate(); 38 | } 39 | 40 | if let Message::CraftMessage(CraftMessage::Initialized) = context.message() { 41 | let installation = craft::markdown::render_markdown(context.props().markdown_text.as_str()); 42 | context.state_mut().markdown = Some(installation); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /crates/craft_core/src/elements/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod container; 2 | pub(crate) mod dropdown; 3 | pub(crate) mod element; 4 | pub(crate) mod stateful_element; 5 | pub(crate) mod empty; 6 | pub(crate) mod image; 7 | pub(crate) mod overlay; 8 | pub(crate) mod slider; 9 | pub(crate) mod switch; 10 | pub(crate) mod text; 11 | pub(crate) mod tinyvg; 12 | 13 | #[allow(clippy::module_inception)] 14 | pub(crate) mod text_input; 15 | 16 | pub(crate) mod canvas; 17 | 18 | pub(crate) mod base_element_state; 19 | pub(crate) mod element_data; 20 | mod element_pre_order_iterator; 21 | pub(crate) mod element_states; 22 | pub(crate) mod element_styles; 23 | pub(crate) mod font; 24 | mod scroll_state; 25 | mod thumb; 26 | mod element_event_impls; 27 | 28 | pub use crate::elements::canvas::Canvas; 29 | pub use crate::elements::container::Container; 30 | pub use crate::elements::dropdown::Dropdown; 31 | pub use crate::elements::element::Element; 32 | pub use crate::elements::stateful_element::StatefulElement; 33 | pub use crate::elements::element::ElementBoxed; 34 | pub use crate::elements::element_data::ElementData; 35 | pub use crate::elements::element_states::ElementState; 36 | pub use crate::elements::element_styles::ElementStyles; 37 | pub use crate::elements::font::Font; 38 | pub use crate::elements::image::Image; 39 | pub use crate::elements::overlay::Overlay; 40 | pub use crate::elements::slider::Slider; 41 | pub use crate::elements::slider::SliderDirection; 42 | pub use crate::elements::switch::Switch; 43 | pub use crate::elements::text::Text; 44 | pub use crate::elements::text_input::TextInput; 45 | pub use crate::elements::text_input::TextInputMessage; 46 | pub use crate::elements::tinyvg::TinyVg; 47 | -------------------------------------------------------------------------------- /crates/craft/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_gui" 3 | description = "A reactive GUI focused on being practical. Inspired by Elm and React. No macros." 4 | version = "0.1.1" 5 | edition = "2024" 6 | license-file = "LICENSE" 7 | homepage = "https://craftgui.com/" 8 | repository = "https://github.com/craft-gui/craft" 9 | readme = "../../README.md" 10 | 11 | [features] 12 | # Force dynamic linking, which improves iterative compile times 13 | dynamic_linking = ["dep:craft_dylib", "craft_core/dynamic_linking"] 14 | devtools = ["craft_core/dev_tools"] 15 | clipboard = ["craft_core/clipboard"] 16 | 17 | png = ["craft_core/png"] 18 | jpeg = ["craft_core/jpeg"] 19 | 20 | accesskit = ["craft_core/accesskit"] 21 | 22 | system_fonts = ["craft_core/system_fonts"] 23 | 24 | http_client = ["craft_resource_manager/http_client"] 25 | vello_renderer = ["craft_renderer/vello_renderer"] 26 | vello_cpu_renderer = ["craft_renderer/vello_cpu_renderer"] 27 | vello_hybrid_renderer = ["craft_renderer/vello_hybrid_renderer"] 28 | vello_hybrid_renderer_webgl = ["craft_renderer/vello_hybrid_renderer_webgl"] 29 | 30 | markdown = ["craft_core/markdown"] 31 | link = ["craft_core/link"] 32 | 33 | code_highlighting = ["craft_core/code_highlighting"] 34 | 35 | default = ["vello_renderer", "http_client", "devtools", "png", "jpeg", "accesskit", "clipboard", "system_fonts"] 36 | 37 | 38 | [dependencies] 39 | craft_dylib = { path = "../craft_dylib", default-features = false, optional = true, version = "0.1.1" } 40 | craft_core = { path = "../craft_core", default-features = false, version = "0.1.1" } 41 | craft_renderer = { path = "../craft_renderer", default-features = false, version = "0.1.1" } 42 | craft_resource_manager = { path = "../craft_resource_manager", default-features = false, version = "0.1.1" } 43 | 44 | 45 | [lib] 46 | -------------------------------------------------------------------------------- /crates/craft_core/src/style/style_flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | #[derive(Clone, Copy, Debug)] 5 | pub struct StyleFlags: u128 { 6 | const FONT_FAMILY_LENGTH = 1 << 0; 7 | const FONT_FAMILY = 1 << 1; 8 | const BOX_SIZING = 1 << 2; 9 | const SCROLLBAR_WIDTH = 1 << 3; 10 | const POSITION = 1 << 4; 11 | const MARGIN = 1 << 5; 12 | const PADDING = 1 << 6; 13 | const GAP = 1 << 7; 14 | const INSET = 1 << 8; 15 | const WIDTH = 1 << 9; 16 | const HEIGHT = 1 << 10; 17 | const MAX_WIDTH = 1 << 11; 18 | const MAX_HEIGHT = 1 << 12; 19 | const MIN_WIDTH = 1 << 13; 20 | const MIN_HEIGHT = 1 << 14; 21 | const X = 1 << 15; 22 | const Y = 1 << 16; 23 | const DISPLAY = 1 << 17; 24 | const WRAP = 1 << 18; 25 | const ALIGN_ITEMS = 1 << 19; 26 | const JUSTIFY_CONTENT = 1 << 20; 27 | const FLEX_DIRECTION = 1 << 21; 28 | const FLEX_GROW = 1 << 22; 29 | const FLEX_SHRINK = 1 << 23; 30 | const FLEX_BASIS = 1 << 24; 31 | const COLOR = 1 << 25; 32 | const BACKGROUND = 1 << 26; 33 | const FONT_SIZE = 1 << 27; 34 | const FONT_WEIGHT = 1 << 28; 35 | const FONT_STYLE = 1 << 29; 36 | const OVERFLOW = 1 << 30; 37 | const BORDER_COLOR = 1 << 31; 38 | const BORDER_WIDTH = 1 << 32; 39 | const BORDER_RADIUS = 1 << 33; 40 | const SCROLLBAR_COLOR = 1 << 34; 41 | const SCROLLBAR_RADIUS = 1 << 35; 42 | const SCROLLBAR_THUMB_MARGIN = 1 << 36; 43 | const VISIBLE = 1 << 37; 44 | const UNDERLINE = 1 << 38; 45 | const SELECTION_COLOR = 1 << 39; 46 | const CURSOR_COLOR = 1 << 40; 47 | const LINE_HEIGHT = 1 << 41; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "crates/craft", 6 | "crates/craft_core", 7 | "crates/craft_dylib", 8 | "crates/craft_logger", 9 | "examples/util", 10 | "examples/request", 11 | "examples/counter", 12 | "examples/text", 13 | "examples/tour", 14 | "examples/events", 15 | "examples/animations", 16 | "examples/overlay", 17 | "examples/custom_event_loop", 18 | "website", 19 | "crates/craft", 20 | "crates/syntect_dumper", "crates/craft_renderer", "crates/craft_primitives", "crates/craft_resource_manager", "crates/craft_runtime", 21 | ] 22 | 23 | [workspace.package] 24 | edition = "2024" 25 | 26 | [profile.release] 27 | opt-level = 3 28 | lto = true 29 | codegen-units = 1 30 | strip = true 31 | debug = false 32 | 33 | [workspace.dependencies] 34 | reqwest = { version = "0.12.19", default-features = false } 35 | tracing = { version = "0.1.41" } 36 | tracing-subscriber = {version = "0.3.19"} 37 | 38 | [workspace.dependencies.peniko] 39 | version = "0.4.0" 40 | default-features = false 41 | features = ["std"] 42 | 43 | 44 | [workspace.dependencies.kurbo] 45 | version = "0.11.2" 46 | default-features = false 47 | features = ["std"] 48 | 49 | [workspace.dependencies.dpi] 50 | version = "0.1.2" 51 | default-features = false 52 | features = [] 53 | 54 | [workspace.dependencies.tokio] 55 | version = "1.45.1" 56 | default-features = false 57 | features = ["sync", "time"] 58 | 59 | [workspace.dependencies.image] 60 | version = "0.25.6" 61 | default-features = false 62 | features = [] 63 | 64 | [workspace.dependencies.tinyvg-rs] 65 | version = "0.0.2" 66 | default-features = false 67 | features = [] 68 | 69 | [workspace.dependencies.chrono] 70 | version = "0.4.41" 71 | default-features = false 72 | features = ["std"] 73 | 74 | [workspace.dependencies.cfg-if] 75 | version = "1.0.1" 76 | 77 | [workspace.dependencies.winit] 78 | version = "0.30.11" 79 | default-features = false 80 | features = [] -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/reactive_tree.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use crate::components::{ComponentId, FocusAction}; 3 | use crate::elements::Element; 4 | use crate::events::update_queue_entry::UpdateQueueEntry; 5 | use crate::reactive::element_state_store::ElementStateStore; 6 | use crate::reactive::state_store::StateStore; 7 | use crate::reactive::tree::ComponentTreeNode; 8 | use std::collections::{HashMap, HashSet, VecDeque}; 9 | use std::rc::Rc; 10 | use crate::animations::animation::AnimationFlags; 11 | use crate::layout::layout_context::LayoutContext; 12 | use crate::reactive::fiber_tree; 13 | use crate::reactive::fiber_tree::FiberNode; 14 | 15 | #[derive(Default)] 16 | pub struct ReactiveTree { 17 | pub element_tree: Option>, 18 | pub(crate) component_tree: Option, 19 | pub(crate) element_ids: HashSet, 20 | pub(crate) component_ids: HashSet, 21 | /// Stores a pointer device id and their pointer captured element. 22 | pub(crate) pointer_captures: HashMap, 23 | pub(crate) update_queue: VecDeque, 24 | pub(crate) user_state: StateStore, 25 | pub(crate) element_state: ElementStateStore, 26 | pub(crate) focus: Option, 27 | pub(crate) previous_animation_flags: AnimationFlags, 28 | pub(crate) taffy_tree: Option>, 29 | } 30 | 31 | impl ReactiveTree { 32 | pub(crate) fn as_fiber_tree(&self) -> Rc> { 33 | fiber_tree::new(self.component_tree.as_ref().unwrap(), self.element_tree.as_ref().unwrap().as_ref()) 34 | } 35 | } 36 | 37 | impl ReactiveTree { 38 | pub(crate) fn update_focus(&mut self, focus: FocusAction) { 39 | match focus { 40 | FocusAction::None => {} 41 | FocusAction::Set(id) => { 42 | self.focus = Some(id); 43 | } 44 | FocusAction::Unset => { 45 | self.focus = None; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/craft_renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_renderer" 3 | version = "0.1.1" 4 | edition.workspace = true 5 | 6 | [features] 7 | vello_renderer = ["dep:vello", "dep:wgpu", "wgpu/fragile-send-sync-non-atomic-wasm"] 8 | vello_cpu_renderer = ["dep:vello_cpu", "dep:softbuffer", "dep:vello_common"] 9 | vello_hybrid_renderer = ["dep:vello_hybrid", "dep:vello_common", "dep:wgpu", "wgpu/fragile-send-sync-non-atomic-wasm"] 10 | vello_hybrid_renderer_webgl = ["wgpu/webgl"] 11 | 12 | [dependencies] 13 | 14 | [dependencies.vello] 15 | git = "https://github.com/linebender/vello.git" 16 | rev = "b1e31b1c4a8a0a3af62827744b2db8150af765f9" 17 | default-features = false 18 | features = ["wgpu"] 19 | optional = true 20 | 21 | [dependencies.vello_cpu] 22 | git = "https://github.com/linebender/vello.git" 23 | rev = "b1e31b1c4a8a0a3af62827744b2db8150af765f9" 24 | optional = true 25 | features = ["multithreading"] 26 | 27 | [dependencies.vello_hybrid] 28 | git = "https://github.com/linebender/vello.git" 29 | rev = "b1e31b1c4a8a0a3af62827744b2db8150af765f9" 30 | default-features = false 31 | features = ["default"] 32 | optional = true 33 | 34 | [dependencies.vello_common] 35 | git = "https://github.com/linebender/vello.git" 36 | rev = "b1e31b1c4a8a0a3af62827744b2db8150af765f9" 37 | optional = true 38 | 39 | [dependencies.softbuffer] 40 | version = "0.4.6" 41 | optional = true 42 | 43 | [dependencies.wgpu] 44 | version = "24.0.3" 45 | default-features = false 46 | features = ["wgsl"] 47 | optional = true 48 | 49 | [dependencies.peniko] 50 | workspace = true 51 | 52 | [dependencies.craft_primitives] 53 | path = "../craft_primitives" 54 | default-features = false 55 | version = "0.1.1" 56 | 57 | 58 | [dependencies.craft_resource_manager] 59 | path = "../craft_resource_manager" 60 | default-features = false 61 | version = "0.1.1" 62 | 63 | [dependencies.winit] 64 | workspace = true 65 | 66 | [dependencies.cfg-if] 67 | workspace = true 68 | 69 | [dependencies.tinyvg-rs] 70 | workspace = true 71 | 72 | [dependencies.chrono] 73 | workspace = true 74 | 75 | -------------------------------------------------------------------------------- /crates/craft_resource_manager/src/identifier.rs: -------------------------------------------------------------------------------- 1 | use crate::ResourceIdentifier::{Bytes, File}; 2 | 3 | #[cfg(feature = "http_client")] 4 | use crate::ResourceIdentifier::Url; 5 | 6 | use std::fmt::Display; 7 | use std::path::PathBuf; 8 | use std::{fmt, fs}; 9 | 10 | #[derive(Clone, Debug, Hash, Eq, PartialEq)] 11 | pub enum ResourceIdentifier { 12 | #[cfg(feature = "http_client")] 13 | Url(String), 14 | File(PathBuf), 15 | Bytes(&'static [u8]), 16 | } 17 | 18 | impl Display for ResourceIdentifier { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | #[cfg(feature = "http_client")] 22 | Url(url) => write!(f, "URL: {url}"), 23 | File(file_path) => write!(f, "File: {:?}", file_path.as_os_str().to_str()), 24 | Bytes(bytes) => write!(f, "Bytes: {:?}", bytes.as_ptr()), 25 | } 26 | } 27 | } 28 | 29 | impl ResourceIdentifier { 30 | pub async fn fetch_data_from_resource_identifier(&self) -> Option> { 31 | match self { 32 | #[cfg(feature = "http_client")] 33 | Url(url) => { 34 | let res = reqwest::get(url).await; 35 | 36 | if let Ok(data) = res { 37 | if !data.status().is_success() { 38 | //tracing::warn!("Failed to fetch the resource from {}, Status: {}", url, data.status()); 39 | return None; 40 | } 41 | 42 | let bytes = data.bytes().await.ok(); 43 | 44 | bytes.map(|b| b.to_vec()) 45 | } else { 46 | None 47 | } 48 | } 49 | File(path) => { 50 | let file = fs::read(path); 51 | if let Ok(data) = file { 52 | return Some(data); 53 | } 54 | //tracing::warn!("Failed to find the local file: {:?}", path.as_os_str().to_str()); 55 | None 56 | } 57 | Bytes(bytes) => Some(bytes.to_vec()), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/corner.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::cornerside::CornerSide; 2 | use crate::geometry::side::Side; 3 | use std::f64::consts::{PI, TAU}; 4 | 5 | #[repr(usize)] 6 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 7 | pub enum Corner { 8 | TopLeft = 0, 9 | TopRight = 1, 10 | BottomRight = 2, 11 | BottomLeft = 3, 12 | } 13 | 14 | impl Corner { 15 | pub(crate) const fn get_primary_side(self) -> Side { 16 | // 000 & 010 = 000 17 | // 001 & 010 = 000 18 | // 011 & 010 = 010 19 | // 010 & 010 = 010 20 | unsafe { std::mem::transmute(self as usize & 2) } 21 | } 22 | 23 | pub(crate) const fn get_secondary_side(self) -> Side { 24 | // 000 -> 011 25 | // 001 -> 001 26 | // 010 -> 001 27 | // 011 -> 011 28 | 29 | let c = self as usize; 30 | let bit = (c ^ (self as usize >> 1)) & 1; 31 | let side_val = 3 - (bit << 1); 32 | 33 | unsafe { std::mem::transmute(side_val) } 34 | } 35 | 36 | pub(crate) fn get_inner_start_side(self) -> CornerSide { 37 | // 000 -> 010 38 | // 001 -> 000 39 | // 010 -> 000 40 | // 011 -> 010 41 | 42 | match self { 43 | Corner::TopLeft => CornerSide::Bottom, 44 | Corner::TopRight => CornerSide::Top, 45 | Corner::BottomRight => CornerSide::Top, 46 | Corner::BottomLeft => CornerSide::Bottom, 47 | } 48 | } 49 | 50 | pub(crate) fn get_outer_start_side(self) -> CornerSide { 51 | match self { 52 | Corner::TopLeft => CornerSide::Top, 53 | Corner::TopRight => CornerSide::Bottom, 54 | Corner::BottomRight => CornerSide::Bottom, 55 | Corner::BottomLeft => CornerSide::Top, 56 | } 57 | } 58 | 59 | pub(crate) fn get_relative_angle(self, angle: f64) -> f64 { 60 | match self { 61 | Corner::TopRight => angle, 62 | Corner::TopLeft => PI - angle, 63 | Corner::BottomLeft => PI + angle, 64 | Corner::BottomRight => TAU - angle, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/empty.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::element::Element; 2 | use crate::elements::element_data::ElementData; 3 | use craft_primitives::geometry::{Point, Rectangle}; 4 | use crate::layout::layout_context::LayoutContext; 5 | use crate::reactive::element_state_store::ElementStateStore; 6 | use craft_renderer::renderer::RenderList; 7 | use crate::text::text_context::TextContext; 8 | use std::any::Any; 9 | use std::sync::Arc; 10 | use kurbo::Affine; 11 | use taffy::{NodeId, TaffyTree}; 12 | use winit::window::Window; 13 | 14 | #[derive(Clone, Default)] 15 | pub struct Empty { 16 | pub element_data: ElementData, 17 | } 18 | 19 | impl Empty { 20 | #[allow(dead_code)] 21 | pub fn new() -> Empty { 22 | Empty { 23 | element_data: Default::default(), 24 | } 25 | } 26 | } 27 | 28 | impl Element for Empty { 29 | fn element_data(&self) -> &ElementData { 30 | &self.element_data 31 | } 32 | 33 | fn element_data_mut(&mut self) -> &mut ElementData { 34 | &mut self.element_data 35 | } 36 | 37 | fn name(&self) -> &'static str { 38 | "Empty" 39 | } 40 | 41 | fn draw( 42 | &mut self, 43 | _renderer: &mut RenderList, 44 | _text_context: &mut TextContext, 45 | _element_state: &mut ElementStateStore, 46 | _pointer: Option, 47 | _window: Option>, 48 | _scale_factor: f64, 49 | ) { 50 | } 51 | 52 | fn compute_layout( 53 | &mut self, 54 | _taffy_tree: &mut TaffyTree, 55 | _element_state: &mut ElementStateStore, 56 | _scale_factor: f64, 57 | ) -> Option { 58 | None 59 | } 60 | 61 | fn finalize_layout( 62 | &mut self, 63 | _taffy_tree: &mut TaffyTree, 64 | _root_node: NodeId, 65 | _position: Point, 66 | _z_index: &mut u32, 67 | _transform: Affine, 68 | _element_state: &mut ElementStateStore, 69 | _pointer: Option, 70 | _text_context: &mut TextContext, 71 | _clip_bounds: Option, 72 | ) { 73 | } 74 | 75 | fn as_any(&self) -> &dyn Any { 76 | self 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | indent_style = "Block" 6 | use_small_heuristics = "Default" 7 | fn_call_width = 200 8 | attr_fn_like_width = 70 9 | struct_lit_width = 18 10 | struct_variant_width = 35 11 | array_width = 200 12 | chain_width = 200 13 | single_line_if_else_max_width = 200 14 | wrap_comments = false 15 | format_code_in_doc_comments = false 16 | doc_comment_code_block_width = 100 17 | comment_width = 80 18 | normalize_comments = false 19 | normalize_doc_attributes = false 20 | format_strings = false 21 | format_macro_matchers = false 22 | format_macro_bodies = true 23 | hex_literal_case = "Preserve" 24 | empty_item_single_line = true 25 | struct_lit_single_line = true 26 | fn_single_line = false 27 | where_single_line = false 28 | imports_indent = "Block" 29 | imports_layout = "Mixed" 30 | imports_granularity = "Preserve" 31 | group_imports = "Preserve" 32 | reorder_imports = true 33 | reorder_modules = true 34 | reorder_impl_items = false 35 | type_punctuation_density = "Wide" 36 | space_before_colon = false 37 | space_after_colon = true 38 | spaces_around_ranges = false 39 | binop_separator = "Front" 40 | remove_nested_parens = true 41 | combine_control_expr = true 42 | short_array_element_width_threshold = 10 43 | overflow_delimited_expr = false 44 | struct_field_align_threshold = 0 45 | enum_discrim_align_threshold = 0 46 | match_arm_blocks = true 47 | match_arm_leading_pipes = "Never" 48 | force_multiline_blocks = false 49 | fn_args_layout = "Tall" 50 | brace_style = "SameLineWhere" 51 | control_brace_style = "AlwaysSameLine" 52 | trailing_semicolon = true 53 | trailing_comma = "Vertical" 54 | match_block_trailing_comma = false 55 | blank_lines_upper_bound = 1 56 | blank_lines_lower_bound = 0 57 | edition = "2024" 58 | version = "One" 59 | inline_attribute_width = 0 60 | format_generated_files = true 61 | merge_derives = true 62 | use_try_shorthand = false 63 | use_field_init_shorthand = false 64 | force_explicit_abi = true 65 | condense_wildcard_suffixes = false 66 | color = "Auto" 67 | required_version = "1.5.1" 68 | unstable_features = false 69 | disable_all_formatting = false 70 | skip_children = false 71 | hide_parse_errors = false 72 | error_on_line_overflow = false 73 | error_on_unformatted = false 74 | ignore = [] 75 | emit_mode = "Files" 76 | make_backup = false 77 | -------------------------------------------------------------------------------- /crates/craft_core/src/events/event_handlers.rs: -------------------------------------------------------------------------------- 1 | // This file is generated via build.rs. Do not modify manually! 2 | 3 | use std::sync::Arc; 4 | 5 | use crate::components::Event; 6 | use crate::elements::Element; 7 | use crate::events::Message; 8 | use crate::reactive::state_store::StateStoreItem; 9 | use crate::{GlobalState, WindowContext}; 10 | 11 | use ui_events::pointer::PointerButtonUpdate; 12 | #[allow(clippy::type_complexity)] 13 | #[derive(Clone, Default)] 14 | pub struct EventHandlers { 15 | pub(crate) on_pointer_up: Option< 16 | Arc< 17 | dyn Fn( 18 | &mut StateStoreItem, 19 | &mut GlobalState, 20 | crate::components::Props, 21 | &mut Event, 22 | &Message, 23 | crate::components::component::ComponentId, 24 | &mut WindowContext, 25 | Option<&dyn Element>, 26 | Option<&dyn Element>, 27 | &PointerButtonUpdate, 28 | ) + Send 29 | + Sync, 30 | >, 31 | >, 32 | pub(crate) on_pointer_down: Option< 33 | Arc< 34 | dyn Fn( 35 | &mut StateStoreItem, 36 | &mut GlobalState, 37 | crate::components::Props, 38 | &mut Event, 39 | &Message, 40 | crate::components::component::ComponentId, 41 | &mut WindowContext, 42 | Option<&dyn Element>, 43 | Option<&dyn Element>, 44 | &PointerButtonUpdate, 45 | ) + Send 46 | + Sync, 47 | >, 48 | >, 49 | pub(crate) on_link_clicked: Option< 50 | Arc< 51 | dyn Fn( 52 | &mut StateStoreItem, 53 | &mut GlobalState, 54 | crate::components::Props, 55 | &mut Event, 56 | &Message, 57 | crate::components::component::ComponentId, 58 | &mut WindowContext, 59 | Option<&dyn Element>, 60 | Option<&dyn Element>, 61 | &str, 62 | ) + Send 63 | + Sync, 64 | >, 65 | >, 66 | } 67 | -------------------------------------------------------------------------------- /crates/syntect_dumper/src/main.rs: -------------------------------------------------------------------------------- 1 | use reqwest::blocking::get; 2 | use std::error::Error; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | use syntect::dumps::dump_to_file; 6 | use syntect::highlighting::ThemeSet; 7 | use syntect::parsing::{SyntaxDefinition, SyntaxSetBuilder}; 8 | 9 | fn main() -> Result<(), Box> { 10 | let cargo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 11 | let syntax_dump_path = cargo_root.join("pack.dump"); 12 | let theme_dir = cargo_root.join("themes"); 13 | 14 | let syntax_sets = [ 15 | "https://raw.githubusercontent.com/sublimehq/Packages/refs/heads/master/Rust/Rust.sublime-syntax", 16 | "https://raw.githubusercontent.com/sublimehq/Packages/refs/heads/master/Markdown/Markdown.sublime-syntax", 17 | "https://raw.githubusercontent.com/sublimehq/Packages/refs/heads/master/ShellScript/Bash.sublime-syntax", 18 | "https://raw.githubusercontent.com/sublimehq/Packages/refs/heads/master/TOML/TOML.sublime-syntax", 19 | ]; 20 | 21 | let mut builder = SyntaxSetBuilder::new(); 22 | builder.add_plain_text_syntax(); 23 | 24 | for syntax_url in &syntax_sets { 25 | let contents = get(*syntax_url)?.text()?; 26 | let syntax_definition = SyntaxDefinition::load_from_str(&contents, false, None)?; 27 | builder.add(syntax_definition); 28 | } 29 | 30 | let syntax_set = builder.build(); 31 | 32 | dump_to_file(&syntax_set, &syntax_dump_path)?; 33 | 34 | let themes = [ 35 | "https://raw.githubusercontent.com/SublimeText/Spacegray/2703e93f559e212ef3895edd10d861a4383ce93d/base16-ocean.dark.tmTheme", 36 | "https://raw.githubusercontent.com/SublimeText/Spacegray/refs/heads/main/Spacegray%20Light.sublime-theme", 37 | ]; 38 | 39 | if !theme_dir.exists() { 40 | fs::create_dir_all(&theme_dir)?; 41 | } 42 | 43 | for theme_url in &themes { 44 | let contents = get(*theme_url)?.text()?; 45 | 46 | // Use the filename from the URL directly 47 | let filename = theme_url.split('/').last().ok_or("Invalid URL, no filename found")?; 48 | 49 | let theme_path = theme_dir.join(filename); 50 | fs::write(&theme_path, contents)?; 51 | } 52 | 53 | let theme_set = ThemeSet::load_from_folder(&theme_dir)?; 54 | let theme_dump_path = cargo_root.join("theme_pack.dump"); 55 | dump_to_file(&theme_set, &theme_dump_path)?; 56 | 57 | println!("Syntax set and themes dumped successfully."); 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/base_element_state.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::element_data::ElementData; 2 | use crate::elements::element_states::ElementState; 3 | use crate::style::Style; 4 | use std::collections::HashMap; 5 | use rustc_hash::FxHashMap; 6 | use smol_str::SmolStr; 7 | use crate::animations::animation::ActiveAnimation; 8 | 9 | #[derive(Debug, Default, Clone)] 10 | pub struct BaseElementState { 11 | pub(crate) hovered: bool, 12 | pub(crate) active: bool, 13 | #[allow(dead_code)] 14 | pub(crate) current_state: ElementState, 15 | /// Whether this element should receive pointer events regardless of hit testing. 16 | /// Useful for scroll thumbs. 17 | pub(crate) pointer_capture: HashMap, 18 | pub(crate) focused: bool, 19 | pub(crate) animations: Option>, 20 | } 21 | 22 | impl<'a> BaseElementState { 23 | pub fn current_style(&self, element_data: &'a ElementData) -> &'a Style { 24 | if self.active { 25 | if let Some(pressed_style) = &element_data.pressed_style { 26 | return pressed_style; 27 | } 28 | } 29 | if self.hovered { 30 | if let Some(hover_style) = &element_data.hover_style { 31 | return hover_style; 32 | } 33 | } 34 | &element_data.style 35 | } 36 | 37 | pub fn current_style_mut(&self, element_data: &'a mut ElementData) -> &'a mut Style { 38 | if self.active { 39 | if let Some(pressed_style) = &mut element_data.pressed_style { 40 | return pressed_style; 41 | } 42 | } 43 | if self.hovered { 44 | if let Some(hover_style) = &mut element_data.hover_style { 45 | return hover_style; 46 | } 47 | } 48 | &mut element_data.style 49 | } 50 | pub fn current_style_mut_no_fallback(&self, element_data: &'a mut ElementData) -> Option<&'a mut Style> { 51 | if self.active { 52 | if let Some(pressed_style) = &mut element_data.pressed_style { 53 | return Some(pressed_style); 54 | } 55 | } 56 | if self.hovered { 57 | if let Some(hover_style) = &mut element_data.hover_style { 58 | return Some(hover_style); 59 | } 60 | } 61 | 62 | None 63 | } 64 | 65 | } 66 | 67 | // HACK: Remove this and all usages when pointer capture per device works. 68 | pub(crate) const DUMMY_DEVICE_ID: i64 = -1; 69 | -------------------------------------------------------------------------------- /examples/counter/main.rs: -------------------------------------------------------------------------------- 1 | use craft::components::Context; 2 | use craft::events::ui_events::pointer::PointerButtonUpdate; 3 | use craft::{ 4 | components::{Component, ComponentSpecification}, 5 | elements::{Container, ElementStyles, Text}, 6 | rgb, 7 | style::{AlignItems, Display, FlexDirection, JustifyContent}, 8 | Color, 9 | }; 10 | 11 | #[derive(Default)] 12 | pub struct Counter { 13 | count: i64, 14 | } 15 | 16 | impl Component for Counter { 17 | type GlobalState = (); 18 | type Props = (); 19 | type Message = (); 20 | 21 | fn view(context: &mut Context) -> ComponentSpecification { 22 | Container::new() 23 | .display(Display::Flex) 24 | .flex_direction(FlexDirection::Column) 25 | .justify_content(JustifyContent::Center) 26 | .align_items(AlignItems::Center) 27 | .width("100%") 28 | .height("100%") 29 | .gap(20) 30 | .push(Text::new(&format!("Count: {}", context.state().count)).font_size(72).color(rgb(50, 50, 50))) 31 | .push( 32 | Container::new() 33 | .display(Display::Flex) 34 | .flex_direction(FlexDirection::Row) 35 | .gap(20) 36 | .push(create_button("-", rgb(244, 67, 54), rgb(211, 47, 47), -1)) 37 | .push(create_button("+", rgb(76, 175, 80), rgb(67, 160, 71), 1)), 38 | ) 39 | .component() 40 | } 41 | } 42 | 43 | fn create_button(label: &str, base_color: Color, hover_color: Color, delta: i64) -> Container { 44 | Container::new() 45 | .border_width(1, 2, 3, 4) 46 | .border_color(rgb(0, 0, 0)) 47 | .border_radius(10, 10, 10, 10) 48 | .padding(15, 30, 15, 30) 49 | .display(Display::Flex) 50 | .justify_content(JustifyContent::Center) 51 | .align_items(AlignItems::Center) 52 | .background(base_color) 53 | .hovered() 54 | .background(hover_color) 55 | .on_pointer_up(move |context: &mut Context, pointer_button: &PointerButtonUpdate| { 56 | if pointer_button.is_primary() { 57 | context.state_mut().count += delta; 58 | context.event_mut().prevent_propagate(); 59 | } 60 | }) 61 | .push(Text::new(label).font_size(24).color(Color::WHITE).disable_selection()) 62 | } 63 | 64 | #[allow(unused)] 65 | #[cfg(not(target_os = "android"))] 66 | fn main() { 67 | use craft::CraftOptions; 68 | util::setup_logging(); 69 | craft::craft_main(Counter::component(), (), CraftOptions::basic("Counter")); 70 | } 71 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/font.rs: -------------------------------------------------------------------------------- 1 | use crate::components::component::ComponentSpecification; 2 | use crate::components::Props; 3 | use crate::elements::element::Element; 4 | use crate::elements::element_data::ElementData; 5 | use crate::generate_component_methods_no_children; 6 | use craft_primitives::geometry::{Point, Rectangle}; 7 | use crate::layout::layout_context::LayoutContext; 8 | use crate::reactive::element_state_store::ElementStateStore; 9 | use craft_renderer::renderer::RenderList; 10 | use craft_resource_manager::ResourceIdentifier; 11 | use crate::text::text_context::TextContext; 12 | use std::any::Any; 13 | use std::sync::Arc; 14 | use kurbo::Affine; 15 | use taffy::{NodeId, TaffyTree}; 16 | use winit::window::Window; 17 | use smol_str::SmolStr; 18 | 19 | #[derive(Clone)] 20 | pub struct Font { 21 | pub(crate) resource_identifier: ResourceIdentifier, 22 | pub element_data: ElementData, 23 | } 24 | 25 | impl Font { 26 | pub fn new(resource_identifier: ResourceIdentifier) -> Self { 27 | Self { 28 | resource_identifier, 29 | element_data: Default::default(), 30 | } 31 | } 32 | 33 | pub fn name() -> &'static str { 34 | "Font" 35 | } 36 | } 37 | 38 | impl Element for Font { 39 | fn element_data(&self) -> &ElementData { 40 | &self.element_data 41 | } 42 | 43 | fn element_data_mut(&mut self) -> &mut ElementData { 44 | &mut self.element_data 45 | } 46 | 47 | fn name(&self) -> &'static str { 48 | "Font" 49 | } 50 | 51 | fn draw( 52 | &mut self, 53 | _renderer: &mut RenderList, 54 | _text_context: &mut TextContext, 55 | _element_state: &mut ElementStateStore, 56 | _pointer: Option, 57 | _window: Option>, 58 | _scale_factor: f64, 59 | ) { 60 | } 61 | 62 | fn compute_layout( 63 | &mut self, 64 | _taffy_tree: &mut TaffyTree, 65 | _element_state: &mut ElementStateStore, 66 | _scale_factor: f64, 67 | ) -> Option { 68 | None 69 | } 70 | 71 | fn finalize_layout( 72 | &mut self, 73 | _taffy_tree: &mut TaffyTree, 74 | _root_node: NodeId, 75 | _position: Point, 76 | _z_index: &mut u32, 77 | _transform: Affine, 78 | _element_state: &mut ElementStateStore, 79 | _pointer: Option, 80 | _text_context: &mut TextContext, 81 | _clip_bounds: Option, 82 | ) { 83 | } 84 | 85 | fn as_any(&self) -> &dyn Any { 86 | self 87 | } 88 | } 89 | 90 | impl Font { 91 | generate_component_methods_no_children!(); 92 | } 93 | -------------------------------------------------------------------------------- /crates/craft_renderer/src/renderer_type.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::sync::Arc; 3 | use winit::window::Window; 4 | use crate::blank_renderer::BlankRenderer; 5 | use crate::renderer::Renderer; 6 | #[cfg(feature = "vello_renderer")] 7 | use crate::vello::VelloRenderer; 8 | 9 | #[cfg(feature = "vello_cpu_renderer")] 10 | use crate::vello_cpu::VelloCpuRenderer; 11 | 12 | #[cfg(feature = "vello_hybrid_renderer")] 13 | use crate::vello_hybrid::VelloHybridRenderer; 14 | 15 | /// An enumeration of the available renderer types for Craft. 16 | /// 17 | /// Depending on compile-time features, different renderers can be enabled. 18 | /// When the `vello_renderer` feature is enabled, the [`Vello`](RendererType::Vello) 19 | /// variant is available; otherwise, the [`Blank`](RendererType::Blank) variant is used. 20 | #[derive(Copy, Clone, Debug)] 21 | pub enum RendererType { 22 | #[cfg(feature = "vello_renderer")] 23 | Vello, 24 | #[cfg(feature = "vello_cpu_renderer")] 25 | VelloCPU, 26 | #[cfg(feature = "vello_hybrid_renderer")] 27 | VelloHybrid, 28 | Blank, 29 | } 30 | 31 | #[allow(clippy::derivable_impls)] 32 | impl Default for RendererType { 33 | fn default() -> Self { 34 | cfg_if::cfg_if! { 35 | if #[cfg(feature = "vello_renderer")] { 36 | RendererType::Vello 37 | } else if #[cfg(feature = "vello_hybrid_renderer")]{ 38 | RendererType::VelloHybrid 39 | } else if #[cfg(feature = "vello_cpu_renderer")]{ 40 | RendererType::VelloCPU 41 | } else { 42 | RendererType::Blank 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl Display for RendererType { 49 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 50 | match self { 51 | #[cfg(feature = "vello_renderer")] 52 | RendererType::Vello => write!(f, "vello/wgpu"), 53 | #[cfg(feature = "vello_cpu_renderer")] 54 | RendererType::VelloCPU => write!(f, "vello/cpu"), 55 | #[cfg(feature = "vello_hybrid_renderer")] 56 | RendererType::VelloHybrid => write!(f, "vello/hybrid"), 57 | RendererType::Blank => write!(f, "blank"), 58 | } 59 | } 60 | } 61 | 62 | impl RendererType { 63 | 64 | pub async fn create(&self, window: Arc) -> Box { 65 | let renderer: Box = match self { 66 | #[cfg(feature = "vello_renderer")] 67 | RendererType::Vello => Box::new(VelloRenderer::new(window, false).await), 68 | #[cfg(feature = "vello_cpu_renderer")] 69 | RendererType::VelloCPU => Box::new(VelloCpuRenderer::new(window)), 70 | #[cfg(feature = "vello_hybrid_renderer")] 71 | RendererType::VelloHybrid => Box::new(VelloHybridRenderer::new(window).await), 72 | RendererType::Blank => Box::new(BlankRenderer), 73 | }; 74 | 75 | renderer 76 | } 77 | } -------------------------------------------------------------------------------- /examples/request/ani_list.rs: -------------------------------------------------------------------------------- 1 | use craft::components::ComponentSpecification; 2 | use craft::elements::{Container, ElementStyles, Image, Text}; 3 | use craft::ResourceIdentifier; 4 | use craft::style::Unit; 5 | use craft::style::{Display, FlexDirection}; 6 | use craft::Color; 7 | use serde::Deserialize; 8 | 9 | #[derive(Debug, Clone, Deserialize, PartialEq)] 10 | pub struct AniListResponse { 11 | pub data: Data, 12 | } 13 | 14 | #[derive(Debug, Clone, Deserialize, PartialEq)] 15 | pub struct Data { 16 | #[serde(rename(deserialize = "Page"))] 17 | pub page: Page, 18 | } 19 | 20 | #[derive(Debug, Clone, Deserialize, PartialEq)] 21 | pub struct Page { 22 | pub media: Vec, 23 | } 24 | 25 | #[derive(Debug, Clone, Deserialize, PartialEq)] 26 | pub struct Media { 27 | pub id: u32, 28 | pub title: Title, 29 | #[serde(rename(deserialize = "coverImage"))] 30 | pub cover_image: CoverImage, 31 | } 32 | 33 | #[derive(Debug, Clone, Deserialize, PartialEq)] 34 | pub struct Title { 35 | pub english: Option, 36 | pub native: Option, 37 | pub romaji: Option, 38 | } 39 | 40 | #[derive(Debug, Clone, Deserialize, PartialEq)] 41 | pub struct CoverImage { 42 | pub large: String, 43 | } 44 | 45 | // Query to use in request 46 | pub const QUERY: &str = " 47 | query { 48 | Page(page: 1, perPage: 10) { 49 | media(type: ANIME, sort: TRENDING_DESC) { 50 | id 51 | title { 52 | romaji 53 | english 54 | native 55 | } 56 | coverImage { 57 | large 58 | } 59 | } 60 | } 61 | } 62 | "; 63 | 64 | pub fn anime_view(media: &Media) -> ComponentSpecification { 65 | let mut title: &str = "No Name"; 66 | 67 | if let Some(native_title) = &media.title.native { 68 | title = native_title; 69 | } 70 | 71 | if let Some(english_title) = &media.title.english { 72 | title = english_title; 73 | } 74 | 75 | if let Some(romaji_title) = &media.title.romaji { 76 | title = romaji_title; 77 | } 78 | 79 | let cover_image = Image::new(ResourceIdentifier::Url(media.cover_image.large.clone())) 80 | .width("185px") 81 | .max_width("185px") 82 | .height("265px") 83 | .max_height("265px") 84 | .border_radius(4.0, 4.0, 4.0, 4.0) 85 | .border_width("1px", "1px", "1px", "1px") 86 | .border_color(Color::from_rgb8(150, 150, 150)); 87 | 88 | let anime_name = Text::new(title) 89 | .max_width(Unit::Percentage(100.0)) 90 | .font_size(14.0) 91 | .color(Color::from_rgb8(50, 50, 50)); 92 | 93 | Container::new() 94 | .display(Display::Flex) 95 | .flex_direction(FlexDirection::Column) 96 | .column_gap("20px") 97 | .push(cover_image) 98 | .push(anime_name) 99 | .width(Unit::Px(185.0)) 100 | .max_height("317px") 101 | .component() 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📜 Craft 2 | [![CI](https://github.com/craft-gui/craft/actions/workflows/ci.yml/badge.svg)](https://github.com/craft-gui/craft/actions/workflows/ci.yml) 3 | [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](./LICENSE) 4 | [![Discord](https://img.shields.io/discord/1382383100562243746?logo=discord&logoColor=%23ffffff&labelColor=%236A7EC2&color=%237389D8)](https://discord.gg/Atb8nuAub2) 5 | 6 | Craft is a reactive GUI. Views are created using Components and Elements. 7 | Updates are performed by handling messages from a Component. 8 | 9 |

10 | The ani list example. 11 | The text editor example 12 |

13 |

14 | The inputs example. 15 | The counter example. 16 |

17 | 18 | ### Counter 19 | 20 | ```rust 21 | use craft::components::{Context, Component, ComponentSpecification}; 22 | use craft::elements::{Container, ElementStyles, Text}; 23 | use craft::events::ui_events::pointer::PointerButtonUpdate; 24 | use craft::style::FlexDirection; 25 | 26 | #[derive(Default)] 27 | pub struct Counter { 28 | count: i64, 29 | } 30 | 31 | impl Component for Counter { 32 | type GlobalState = (); 33 | type Props = (); 34 | type Message = (); 35 | 36 | fn view(context: &mut Context) -> ComponentSpecification { 37 | fn button(label: &str, delta: i64) -> Container { 38 | Container::new() 39 | .on_pointer_up(move |c: &mut Context, _: &PointerButtonUpdate| { 40 | c.state_mut().count += delta; 41 | }) 42 | .push(Text::new(label).disable_selection().padding(10, 10, 10, 10)) 43 | } 44 | 45 | Container::new() 46 | .push(Text::new(&format!("Count: {}", context.state().count)).disable_selection()) 47 | .flex_direction(FlexDirection::Column) 48 | .push( 49 | Container::new() 50 | .flex_direction(FlexDirection::Row) 51 | .push(button("-", -1)) 52 | .push(button("+", 1)) 53 | ).component() 54 | } 55 | } 56 | 57 | fn main() { 58 | craft::craft_main(Counter::component(), (), craft::CraftOptions::basic("Counter")); 59 | } 60 | 61 | ``` 62 | 63 | ## Goals 64 | 65 | * Reactive 66 | * Components 67 | * Pure Rust without procedural macros 68 | * Web-like styling 69 | * Cross Platform 70 | 71 | # Features 72 | 73 | * ✅ Reactive Components 74 | * ✅ Async Updates 75 | * ✅ Text Rendering 76 | * ✅ Windows/Linux 77 | * ✅ Android(basic) 78 | * ✅ Web(basic) 79 | * ✅ Image Support 80 | * ✅ DPI Scaling Support 81 | * ⬜️ Transform (Rotation, Skew, Scale) Support 82 | * ✅ Mac 83 | * ⬜️ iOS 84 | * ✅ Text Input (Basic) 85 | * ✅ IME Support (Basic) 86 | * ✅ Animations (Basic) 87 | * ✅ Scrollables (Basic) 88 | * ⬜️ Documentation 89 | * ⬜️ Tests 90 | * ⬜ Videos 91 | * ⬜ SVGs 92 | * ✅ Accessibility (Very Basic) 93 | 94 | ## Run Examples: 95 | 96 | ```shell 97 | cargo run --package counter 98 | ``` 99 | -------------------------------------------------------------------------------- /examples/custom_event_loop/main.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_event_loop::CraftWinitState; 2 | use craft::components::Context; 3 | use craft::elements::Canvas; 4 | use craft::events::ui_events::pointer::PointerButtonUpdate; 5 | use craft::{ 6 | components::{Component, ComponentSpecification}, 7 | elements::{Container, ElementStyles, Text}, 8 | rgb, 9 | style::{AlignItems, Display, FlexDirection, JustifyContent}, 10 | Color, 11 | }; 12 | use craft::setup_craft; 13 | use winit::event_loop::EventLoop; 14 | 15 | mod custom_event_loop; 16 | mod wgpu_triangle; 17 | 18 | #[derive(Default)] 19 | pub struct Counter { 20 | count: i64, 21 | } 22 | 23 | impl Component for Counter { 24 | type GlobalState = (); 25 | type Props = (); 26 | type Message = (); 27 | 28 | fn view(context: &mut Context) -> ComponentSpecification { 29 | Container::new() 30 | .display(Display::Flex) 31 | .flex_direction(FlexDirection::Column) 32 | .justify_content(JustifyContent::Center) 33 | .align_items(AlignItems::Center) 34 | .width("100%") 35 | .height("100%") 36 | .gap(20) 37 | .push(Text::new(&format!("Count: {}", context.state().count)).font_size(72).color(rgb(50, 50, 50))) 38 | .push( 39 | Container::new() 40 | .display(Display::Flex) 41 | .flex_direction(FlexDirection::Row) 42 | .gap(20) 43 | .push(create_button("-", rgb(244, 67, 54), rgb(211, 47, 47), -1)) 44 | .push(create_button("+", rgb(76, 175, 80), rgb(67, 160, 71), 1)), 45 | ) 46 | .push(Canvas::new().width("300px").height("300px").min_width("300px").min_height("300px")) 47 | .component() 48 | } 49 | } 50 | 51 | fn create_button(label: &str, base_color: Color, hover_color: Color, delta: i64) -> Container { 52 | Container::new() 53 | .border_width(1, 2, 3, 4) 54 | .border_color(rgb(0, 0, 0)) 55 | .border_radius(10, 10, 10, 10) 56 | .padding(15, 30, 15, 30) 57 | .display(Display::Flex) 58 | .justify_content(JustifyContent::Center) 59 | .align_items(AlignItems::Center) 60 | .background(base_color) 61 | .hovered() 62 | .background(hover_color) 63 | .on_pointer_up( 64 | move |context: &mut Context, pointer_button: &PointerButtonUpdate| { 65 | if pointer_button.is_primary() { 66 | context.state_mut().count += delta; 67 | context.event_mut().prevent_propagate(); 68 | } 69 | }, 70 | ) 71 | .push(Text::new(label).font_size(24).color(Color::WHITE).disable_selection()) 72 | } 73 | 74 | #[allow(unused)] 75 | #[cfg(not(target_os = "android"))] 76 | fn main() { 77 | use craft::CraftOptions; 78 | 79 | let application = Counter::component(); 80 | let global_state = (); 81 | let options = CraftOptions::basic("Custom Event Loop"); 82 | 83 | 84 | let event_loop = EventLoop::new().expect("Failed to create winit event loop."); 85 | let craft_state = setup_craft(application, Box::new(global_state), Some(options)); 86 | let mut winit_craft_state = CraftWinitState::new(craft_state); 87 | 88 | event_loop.run_app(&mut winit_craft_state).expect("run_app failed"); 89 | } 90 | -------------------------------------------------------------------------------- /crates/craft_core/src/view_introspection.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::{Font, Image, TinyVg}; 2 | use crate::events::internal::InternalMessage; 3 | use crate::reactive::fiber_tree::FiberNode; 4 | use craft_resource_manager::resource_type::ResourceType; 5 | use craft_resource_manager::{ResourceIdentifier, ResourceManager}; 6 | use craft_runtime::Sender; 7 | use std::cell::RefCell; 8 | use std::collections::HashMap; 9 | use std::rc::Rc; 10 | use std::sync::Arc; 11 | 12 | /// Introspect the view. 13 | /// 14 | // Scans through the component tree and diffs it for resources that need to be updated. 15 | pub fn scan_view_for_resources( 16 | app_sender: Sender, 17 | fiber_tree: Rc>, 18 | resource_manager: Arc, 19 | resources_collected: &mut HashMap, 20 | ) { 21 | let mut nodes: Vec>> = Vec::new(); 22 | let mut to_visit: Vec>> = vec![Rc::clone(&fiber_tree)]; 23 | 24 | while let Some(node) = to_visit.pop() { 25 | if node.borrow().element.is_some() { 26 | nodes.push(Rc::clone(&node)); 27 | } 28 | for child in node.borrow().children.iter().rev() { 29 | to_visit.push(Rc::clone(&child)); 30 | } 31 | } 32 | 33 | for fiber_node in nodes { 34 | if let Some(element) = fiber_node.borrow().element { 35 | let image_resource = 36 | element.as_any().downcast_ref::().map(|image| image.resource_identifier.clone()); 37 | 38 | let font_resource = element.as_any().downcast_ref::().map(|font| font.resource_identifier.clone()); 39 | let tinyvg_resource = 40 | element.as_any().downcast_ref::().map(|tinyvg| tinyvg.resource_identifier.clone()); 41 | 42 | if image_resource.is_some() || font_resource.is_some() || tinyvg_resource.is_some() { 43 | if let Some(image_resource) = image_resource { 44 | resource_manager.async_download_resource_and_send_message_on_finish( 45 | app_sender.clone(), 46 | image_resource.clone(), 47 | ResourceType::Image, 48 | resources_collected, 49 | ); 50 | resources_collected.insert(image_resource.clone(), true); 51 | } 52 | 53 | if let Some(font_resource) = font_resource { 54 | resource_manager.async_download_resource_and_send_message_on_finish( 55 | app_sender.clone(), 56 | font_resource.clone(), 57 | ResourceType::Font, 58 | resources_collected, 59 | ); 60 | resources_collected.insert(font_resource.clone(), true); 61 | } 62 | 63 | if let Some(tinyvg_resource) = tinyvg_resource { 64 | resource_manager.async_download_resource_and_send_message_on_finish( 65 | app_sender.clone(), 66 | tinyvg_resource.clone(), 67 | ResourceType::TinyVg, 68 | resources_collected, 69 | ); 70 | resources_collected.insert(tinyvg_resource.clone(), true); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/text/main.rs: -------------------------------------------------------------------------------- 1 | use util::setup_logging; 2 | 3 | use craft::components::ComponentSpecification; 4 | use craft::components::Component; 5 | use craft::components::Context; 6 | use craft::elements::ElementStyles; 7 | use craft::elements::TextInput; 8 | use craft::elements::{Container, Font, Text}; 9 | use craft::ResourceIdentifier; 10 | use craft::style::Display::Block; 11 | use craft::style::Overflow::Scroll; 12 | use craft::style::Unit; 13 | use craft::style::{FlexDirection, FontStyle, TextStyleProperty, Weight}; 14 | use craft::text::RangedStyles; 15 | use craft::{palette, CraftOptions}; 16 | use craft::{craft_main, rgb}; 17 | 18 | #[derive(Default, Copy, Clone)] 19 | pub struct TextState {} 20 | 21 | const FONT: &str = 22 | "https://github.com/google/material-design-icons/raw/refs/heads/master/variablefont/MaterialSymbolsOutlined%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf"; 23 | 24 | impl Component for TextState { 25 | type GlobalState = (); 26 | type Props = (); 27 | type Message = (); 28 | 29 | fn view(_context: &mut Context) -> ComponentSpecification { 30 | let text = "Rich text includes color, bold, italic, links, underline, and background."; 31 | let mut rich_text = TextInput::new(text) 32 | .border_width(0, 0, 0, 0) 33 | .disabled() 34 | .on_link_clicked( 35 | move |context: &mut Context, link: &str| { 36 | println!("Link clicked: {link}"); 37 | context.event_mut().prevent_propagate(); 38 | }); 39 | 40 | let ranged_styles = vec![ 41 | (19..24, TextStyleProperty::Color(rgb(255, 0, 0))), 42 | (26..30, TextStyleProperty::FontWeight(Weight::BOLD)), 43 | (32..38, TextStyleProperty::FontStyle(FontStyle::Italic)), 44 | (40..45, TextStyleProperty::Link("craftgui.com".to_string())), 45 | (47..56, TextStyleProperty::UnderlineBrush(rgb(255, 0, 0))), 46 | (47..56, TextStyleProperty::UnderlineSize(1.0)), 47 | (47..56, TextStyleProperty::UnderlineOffset(-1.0)), 48 | (47..56, TextStyleProperty::Underline(true)), 49 | (62..72, TextStyleProperty::BackgroundColor(rgb(200, 200, 200))), 50 | ]; 51 | let ranged_styles = RangedStyles::new(ranged_styles); 52 | rich_text.ranged_styles = Some(ranged_styles); 53 | 54 | 55 | Container::new() 56 | .height(Unit::Px(500.0)) 57 | .display(Block) 58 | .flex_direction(FlexDirection::Row) 59 | .push(Text::new("Hello, World!")) 60 | .push(rich_text) 61 | .push(Font::new(ResourceIdentifier::Url(FONT.to_string()))) 62 | .push(Text::new("search home").font_family("Material Symbols Outlined").font_size(24.0)) 63 | .push( 64 | TextInput::new(include_str!("../counter/main.rs")) 65 | .height(Unit::Px(600.0)) 66 | .selection_color(palette::css::RED) 67 | .cursor_color(palette::css::BLUE) 68 | .width(Unit::Px(800.0)) 69 | .display(Block) 70 | .overflow(Scroll), 71 | ) 72 | .push(Text::new("search home").font_family("Material Symbols Outlined").font_size(24.0)) 73 | .component() 74 | } 75 | } 76 | 77 | #[allow(dead_code)] 78 | fn main() { 79 | setup_logging(); 80 | craft_main(TextState::component(), (), CraftOptions::basic("Text")); 81 | } 82 | -------------------------------------------------------------------------------- /crates/craft_core/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod mouse_wheel; 2 | 3 | pub(crate) mod event_dispatch; 4 | pub mod internal; 5 | pub mod update_queue_entry; 6 | pub(crate) mod event_handlers; 7 | //#[cfg(test)] 8 | //mod tests; 9 | 10 | pub use mouse_wheel::MouseWheel; 11 | pub use winit::event::ElementState; 12 | 13 | use crate::components::ComponentId; 14 | use crate::elements::Element; 15 | use crate::events::CraftMessage::PointerButtonUp; 16 | use std::any::Any; 17 | use std::sync::Arc; 18 | pub use ui_events; 19 | use ui_events::keyboard::KeyboardEvent; 20 | use ui_events::pointer::{PointerButtonUpdate, PointerScrollUpdate, PointerUpdate}; 21 | pub use winit::event::Ime; 22 | pub use winit::event::Modifiers; 23 | pub use winit::event::MouseButton; 24 | use crate::utils::cloneable_any::CloneableAny; 25 | 26 | pub type ElementFilter = dyn Fn(&dyn Element) -> bool + Send + Sync + 'static; 27 | 28 | #[derive(Clone)] 29 | pub enum EventDispatchType { 30 | Bubbling, 31 | Direct(ComponentId), 32 | /// Sends the message to all elements that satisfy the given predicate function. 33 | /// The predicate should return `true` for an element to receive the message. 34 | DirectToMatchingElements(Arc), 35 | Accesskit(ComponentId), 36 | } 37 | 38 | #[derive(Clone)] 39 | pub enum CraftMessage { 40 | Initialized, 41 | PointerButtonUp(PointerButtonUpdate), 42 | PointerButtonDown(PointerButtonUpdate), 43 | KeyboardInputEvent(KeyboardEvent), 44 | PointerMovedEvent(PointerUpdate), 45 | PointerScroll(PointerScrollUpdate), 46 | ImeEvent(Ime), 47 | TextInputChanged(String), 48 | LinkClicked(String), 49 | /// Generated when a dropdown is opened or closed. The boolean is the status of is_open after the event has occurred. 50 | DropdownToggled(bool), 51 | /// The index of the item selected in the list. 52 | /// For example, if you select the first item the index will be 0. 53 | DropdownItemSelected(usize), 54 | /// Generated when a switch is toggled. The boolean is the status of toggled after the event has occurred. 55 | SwitchToggled(bool), 56 | SliderValueChanged(f64), 57 | ElementMessage(Arc), 58 | } 59 | 60 | impl CraftMessage { 61 | pub fn clicked(&self) -> bool { 62 | if let PointerButtonUp(pointer_button) = self && pointer_button.is_primary() { 63 | return true; 64 | } 65 | 66 | false 67 | } 68 | 69 | pub fn new_element_message(data: T) -> CraftMessage 70 | where 71 | T: Any + Send + Sync + Clone, 72 | { 73 | Self::ElementMessage(Arc::new(data)) 74 | } 75 | } 76 | pub type UserMessage = dyn CloneableAny; 77 | 78 | pub enum Message { 79 | CraftMessage(CraftMessage), 80 | #[cfg(target_arch = "wasm32")] 81 | UserMessage(Box), 82 | #[cfg(not(target_arch = "wasm32"))] 83 | UserMessage(Box), 84 | } 85 | 86 | impl Clone for Message { 87 | fn clone(&self) -> Self { 88 | match self { 89 | Message::CraftMessage(msg) => Message::CraftMessage(msg.clone()), 90 | Message::UserMessage(msg) => Message::UserMessage(msg.as_ref().clone_box()), 91 | } 92 | } 93 | } 94 | 95 | impl Message { 96 | pub fn clicked(&self) -> bool { 97 | if let Message::CraftMessage(message) = self { 98 | return message.clicked(); 99 | } 100 | 101 | false 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/element_pre_order_iterator.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::element::Element; 2 | pub struct ElementTreePreOrderIterator<'a> { 3 | stack: Vec<&'a dyn Element>, 4 | } 5 | 6 | impl<'a> ElementTreePreOrderIterator<'a> { 7 | fn new(root: &'a dyn Element) -> Self { 8 | Self { stack: vec![root] } 9 | } 10 | } 11 | 12 | impl<'a> Iterator for ElementTreePreOrderIterator<'a> { 13 | type Item = &'a dyn Element; 14 | 15 | fn next(&mut self) -> Option { 16 | if let Some(node) = self.stack.pop() { 17 | for child in node.children().iter().rev() { 18 | self.stack.push(child.internal.as_ref()); 19 | } 20 | Some(node) 21 | } else { 22 | None 23 | } 24 | } 25 | } 26 | 27 | impl dyn Element { 28 | pub fn pre_order_iter(&self) -> ElementTreePreOrderIterator { 29 | ElementTreePreOrderIterator::new(self) 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use std::any::Any; 36 | use crate::elements::element::ElementBoxed; 37 | use crate::elements::{Container, Text}; 38 | use crate::events::update_queue_entry::UpdateQueueEntry; 39 | use crate::reactive::element_id::reset_unique_element_id; 40 | use crate::reactive::element_state_store::ElementStateStore; 41 | use crate::reactive::state_store::StateStore; 42 | use crate::reactive::tree::diff_trees; 43 | use crate::text::text_context::TextContext; 44 | use crate::window_context::WindowContext; 45 | use crate::GlobalState; 46 | use std::collections::VecDeque; 47 | 48 | #[test] 49 | fn pre_order_iter_ids_correct_order() { 50 | let mut text_context = TextContext::new(); 51 | reset_unique_element_id(); 52 | 53 | let initial_view = Container::new().id("1").component().push(Text::new("Foo").id("2").component()).push( 54 | Container::new() 55 | .id("3") 56 | .component() 57 | .push(Text::new("Bar").id("4").component()) 58 | .push(Text::new("Baz").id("5").component()), 59 | ); 60 | let root_element: ElementBoxed = Container::new().id("0").into(); 61 | 62 | let mut user_state = StateStore::default(); 63 | let mut element_state = ElementStateStore::default(); 64 | let mut global_state = GlobalState::from(Box::new(()) as Box); 65 | let mut window_context = WindowContext::new(); 66 | let mut update_queue: VecDeque = VecDeque::new(); 67 | 68 | let initial_tree = diff_trees( 69 | initial_view, 70 | root_element.clone(), 71 | None, 72 | &mut user_state, 73 | &mut global_state, 74 | &mut element_state, 75 | false, 76 | &mut text_context, 77 | 1.0, 78 | &mut window_context, 79 | &mut update_queue, 80 | ); 81 | 82 | let mut iter = initial_tree.element_tree.internal.pre_order_iter(); 83 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("0".into())); 84 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("1".into())); 85 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("2".into())); 86 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("3".into())); 87 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("4".into())); 88 | assert_eq!(iter.next().unwrap().get_id().clone(), Some("5".into())); 89 | assert!(iter.next().is_none()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /website/src/navbar.rs: -------------------------------------------------------------------------------- 1 | use crate::theme::{wrapper, NAVBAR_BACKGROUND_COLOR, NAVBAR_TEXT_COLOR, NAVBAR_TEXT_HOVERED_COLOR}; 2 | use crate::WebsiteGlobalState; 3 | use craft::components::{Component, ComponentSpecification, Context}; 4 | use craft::elements::{Container, ElementStyles, Text}; 5 | use craft::style::{AlignItems, Display, JustifyContent, Unit, Weight}; 6 | use craft::Color; 7 | 8 | #[derive(Default)] 9 | pub(crate) struct Navbar {} 10 | 11 | pub const NAVBAR_HEIGHT: f32 = 60.0; 12 | 13 | fn create_link(label: &str, route: &str) -> Text { 14 | Text::new(label) 15 | .id(format!("route_{route}").as_str()) 16 | .margin("0px", "12px", "0px", "0px") 17 | .font_size(16.0) 18 | .disable_selection() 19 | .color(NAVBAR_TEXT_COLOR) 20 | .hovered() 21 | .color(NAVBAR_TEXT_HOVERED_COLOR) 22 | .underline(1.0, Color::BLACK, None) 23 | .margin("0px", "12px", "0px", "0px") 24 | .font_size(16.5) 25 | .disable_selection() 26 | .normal() 27 | } 28 | 29 | impl Component for Navbar { 30 | type GlobalState = WebsiteGlobalState; 31 | type Props = (); 32 | type Message = (); 33 | 34 | fn view(_context: &mut Context) -> ComponentSpecification { 35 | let container = Container::new() 36 | .width("100%") 37 | .height(Unit::Px(NAVBAR_HEIGHT)) 38 | .min_height(Unit::Px(NAVBAR_HEIGHT)) 39 | .max_height(Unit::Px(NAVBAR_HEIGHT)) 40 | .border_width("0px", "0px", "2px", "0px") 41 | .border_color(Color::from_rgb8(240, 240, 240)) 42 | .background(NAVBAR_BACKGROUND_COLOR); 43 | 44 | let wrapper = wrapper() 45 | .display(Display::Flex) 46 | .justify_content(JustifyContent::SpaceBetween) 47 | .align_items(AlignItems::Center) 48 | // Left 49 | .push( 50 | Container::new() 51 | .display(Display::Flex) 52 | .justify_content(JustifyContent::Center) 53 | .align_items(AlignItems::Center) 54 | .push( 55 | create_link("Craft", "/") 56 | .font_size(32.0) 57 | .font_weight(Weight::BOLD) 58 | .margin("0px", "24px", "0px", "0px") 59 | .hovered() 60 | .font_size(32.0) 61 | .font_weight(Weight::BOLD) 62 | .margin("0px", "24px", "0px", "0px"), 63 | ) 64 | .push(create_link("Home", "/").margin("0px", "12px", "0px", "0px")) 65 | .push(create_link("Docs", "/docs").margin("0px", "12px", "0px", "0px")) 66 | .push(create_link("Examples", "/examples").margin("0px", "12px", "0px", "0px")) 67 | ) 68 | .component(); 69 | 70 | container.push(wrapper).component() 71 | } 72 | 73 | fn update(context: &mut Context) { 74 | if !context.message().clicked() { 75 | return; 76 | } 77 | 78 | let id = context.target().and_then(|e| e.get_id().map(|s| s.to_string())); 79 | if let Some(current_target) = id { 80 | if current_target.starts_with("route_") { 81 | let route = current_target.trim_start_matches("route_"); 82 | context.global_state_mut().set_route(route); 83 | context.event_mut().prevent_propagate(); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/craft_core/src/devtools/dev_tools_component.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{Component, ComponentId, ComponentSpecification}; 2 | use crate::components::{Context, Props}; 3 | use crate::devtools::dev_tools_colors::CONTAINER_BACKGROUND_COLOR; 4 | use crate::devtools::dev_tools_element::DevTools; 5 | use crate::devtools::layout_window::{LayoutWindow, LayoutWindowProps}; 6 | use crate::devtools::tree_window::tree_window; 7 | use crate::elements::element::Element; 8 | use crate::elements::ElementStyles; 9 | use crate::events::{CraftMessage, Message}; 10 | use crate::style::Display::Flex; 11 | use crate::style::{FlexDirection, Unit}; 12 | 13 | #[derive(Default)] 14 | pub(crate) struct DevToolsComponent { 15 | pub selected_element: Option, 16 | pub inspector_hovered_element: Option, 17 | } 18 | 19 | impl Component for DevToolsComponent { 20 | type GlobalState = (); 21 | type Props = Option>; 22 | type Message = (); 23 | 24 | fn view(context: &mut Context) -> ComponentSpecification { 25 | let root = context.props().as_ref().unwrap().clone(); 26 | let element_tree = tree_window(root.as_ref(), context.state().selected_element); 27 | 28 | // Find the selected element in the element tree, so that we can inspect their style values. 29 | let mut selected_element: Option<&dyn Element> = None; 30 | if context.state().selected_element.is_some() { 31 | for element in root.pre_order_iter().collect::>().iter().rev() { 32 | if element.component_id() != context.state().selected_element.unwrap() { 33 | continue; 34 | } 35 | 36 | selected_element = Some(*element); 37 | break; 38 | } 39 | } 40 | 41 | let styles_window = LayoutWindow::component().props(Props::new(LayoutWindowProps { 42 | selected_element: selected_element.map(|e| e.clone_box()), 43 | })); 44 | 45 | DevTools::new() 46 | .display(Flex) 47 | .push_debug_inspector_tree(root) 48 | .push_selected_inspector_element(context.state().selected_element) 49 | .push_hovered_inspector_element(context.state().inspector_hovered_element) 50 | .flex_direction(FlexDirection::Column) 51 | .background(CONTAINER_BACKGROUND_COLOR) 52 | .width(Unit::Percentage(100.0)) 53 | .height(Unit::Percentage(100.0)) 54 | .max_height(Unit::Percentage(100.0)) 55 | .push(element_tree) 56 | .push(styles_window) 57 | .component() 58 | } 59 | 60 | fn update(context: &mut Context) { 61 | if let Some(id) = context.current_target().and_then(|e| e.get_id().clone()) { 62 | if !id.contains("tree_view_") { 63 | return; 64 | } 65 | 66 | let id = id.trim_start_matches("tree_view_").to_owned(); 67 | 68 | // Set the selected element in the element tree inspector. 69 | if context.message().clicked() { 70 | let component_id: ComponentId = id.parse().unwrap(); 71 | context.state_mut().selected_element = Some(component_id); 72 | } 73 | 74 | // Update the hovered element in the inspector tree, so that the DevTools widget can draw a debug overlay. 75 | if let Message::CraftMessage(CraftMessage::PointerMovedEvent(_pointer_moved_event)) = context.message() { 76 | let component_id: ComponentId = id.parse().unwrap(); 77 | context.state_mut().inspector_hovered_element = Some(component_id); 78 | } 79 | } 80 | } 81 | } 82 | 83 | pub fn dev_tools_view(root: Box) -> ComponentSpecification { 84 | DevToolsComponent::component().props(Props::new(Some(root))) 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUST_STABLE_VER: "1.88" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 14 | 15 | jobs: 16 | build-linux: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | - name: Install Stable Toolchain 22 | uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: ${{ env.RUST_STABLE_VER }} 25 | - name: Build Craft 26 | run: cargo build --verbose 27 | - name: Build Counter Example 28 | run: cargo build --package counter 29 | - name: Build Request Example 30 | run: cargo build --package request 31 | - name: Build Text Example 32 | run: cargo build --package text 33 | 34 | build-macos: 35 | runs-on: macos-latest 36 | steps: 37 | - name: Checkout Repository 38 | uses: actions/checkout@v4 39 | - name: Install Stable Toolchain 40 | uses: dtolnay/rust-toolchain@master 41 | with: 42 | toolchain: ${{ env.RUST_STABLE_VER }} 43 | - name: Build Craft 44 | run: cargo build --verbose 45 | - name: Build Counter Example 46 | run: cargo build --package counter 47 | 48 | build-windows: 49 | runs-on: windows-latest 50 | steps: 51 | - name: Checkout Repository 52 | uses: actions/checkout@v4 53 | - name: Install Stable Toolchain 54 | uses: dtolnay/rust-toolchain@master 55 | with: 56 | toolchain: ${{ env.RUST_STABLE_VER }} 57 | - name: Build Counter Example 58 | run: cargo build --bin counter 59 | 60 | build-android: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout Repository 64 | uses: actions/checkout@v4 65 | 66 | - name: Install Stable Toolchain 67 | uses: dtolnay/rust-toolchain@master 68 | with: 69 | toolchain: ${{ env.RUST_STABLE_VER }} 70 | targets: aarch64-linux-android 71 | 72 | - name: Install Dependencies 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y libssl-dev pkg-config curl unzip 76 | 77 | - name: Set up JDK 17 78 | uses: actions/setup-java@v3 79 | with: 80 | java-version: '17' 81 | distribution: 'temurin' 82 | 83 | - name: Setup Android SDK 84 | uses: android-actions/setup-android@v3 85 | with: 86 | packages: 'platforms;android-30 ndk;27.2.12479018' 87 | 88 | - name: Install cargo APK 89 | run: cargo install cargo-apk 90 | 91 | - name: Build Counter Example for Android 92 | run: cargo apk build --package counter --lib 93 | 94 | build-ios: 95 | runs-on: macos-latest 96 | steps: 97 | - name: Checkout Repository 98 | uses: actions/checkout@v4 99 | - name: Install Stable Toolchain 100 | uses: dtolnay/rust-toolchain@master 101 | with: 102 | toolchain: ${{ env.RUST_STABLE_VER }} 103 | targets: aarch64-apple-ios 104 | - name: Build Counter Example for iOS 105 | run: cargo build --target aarch64-apple-ios --bin counter 106 | 107 | unit-tests: 108 | runs-on: ubuntu-latest 109 | steps: 110 | - name: Checkout Repository 111 | uses: actions/checkout@v4 112 | - name: Install Stable Toolchain 113 | uses: dtolnay/rust-toolchain@master 114 | with: 115 | toolchain: ${{ env.RUST_STABLE_VER }} 116 | - name: Build Unit Tests 117 | run: cargo test --package craft_core --no-run 118 | - name: Run Unit Tests 119 | run: cargo test --package craft_core --no-fail-fast --verbose 120 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/image.rs: -------------------------------------------------------------------------------- 1 | use crate::components::component::ComponentSpecification; 2 | use crate::components::Props; 3 | use crate::elements::element::Element; 4 | use crate::elements::element_data::ElementData; 5 | use crate::elements::ElementStyles; 6 | use crate::generate_component_methods_no_children; 7 | use craft_primitives::geometry::{Point, Rectangle}; 8 | use crate::layout::layout_context::{ImageContext, LayoutContext}; 9 | use crate::reactive::element_state_store::ElementStateStore; 10 | use craft_renderer::RenderList; 11 | use craft_resource_manager::ResourceIdentifier; 12 | use crate::style::Style; 13 | use crate::text::text_context::TextContext; 14 | use std::any::Any; 15 | use std::sync::Arc; 16 | use kurbo::Affine; 17 | use taffy::{NodeId, TaffyTree}; 18 | use winit::window::Window; 19 | use smol_str::SmolStr; 20 | 21 | #[derive(Clone)] 22 | pub struct Image { 23 | pub(crate) resource_identifier: ResourceIdentifier, 24 | pub element_data: ElementData, 25 | } 26 | 27 | impl Image { 28 | pub fn new(resource_identifier: ResourceIdentifier) -> Image { 29 | Image { 30 | resource_identifier, 31 | element_data: Default::default(), 32 | } 33 | } 34 | 35 | pub fn name() -> &'static str { 36 | "Image" 37 | } 38 | } 39 | 40 | impl Element for Image { 41 | fn element_data(&self) -> &ElementData { 42 | &self.element_data 43 | } 44 | 45 | fn element_data_mut(&mut self) -> &mut ElementData { 46 | &mut self.element_data 47 | } 48 | 49 | fn name(&self) -> &'static str { 50 | "Image" 51 | } 52 | 53 | fn draw( 54 | &mut self, 55 | renderer: &mut RenderList, 56 | _text_context: &mut TextContext, 57 | element_state: &mut ElementStateStore, 58 | _pointer: Option, 59 | _window: Option>, 60 | scale_factor: f64, 61 | ) { 62 | if !self.element_data.style.visible() { 63 | return; 64 | } 65 | let computed_box_transformed = self.computed_box_transformed(); 66 | let content_rectangle = computed_box_transformed.content_rectangle(); 67 | self.draw_borders(renderer, element_state, scale_factor); 68 | 69 | renderer.draw_image(content_rectangle.scale(scale_factor), self.resource_identifier.clone()); 70 | } 71 | 72 | fn compute_layout( 73 | &mut self, 74 | taffy_tree: &mut TaffyTree, 75 | _element_state: &mut ElementStateStore, 76 | _scale_factor: f64, 77 | ) -> Option { 78 | self.merge_default_style(); 79 | let style: taffy::Style = self.element_data.style.to_taffy_style(); 80 | 81 | self.element_data.layout_item.build_tree_with_context( 82 | taffy_tree, 83 | style, 84 | LayoutContext::Image(ImageContext { 85 | resource_identifier: self.resource_identifier.clone(), 86 | }), 87 | ) 88 | } 89 | 90 | fn finalize_layout( 91 | &mut self, 92 | taffy_tree: &mut TaffyTree, 93 | root_node: NodeId, 94 | position: Point, 95 | z_index: &mut u32, 96 | transform: Affine, 97 | element_state: &mut ElementStateStore, 98 | _pointer: Option, 99 | _text_context: &mut TextContext, 100 | clip_bounds: Option, 101 | ) { 102 | let result = taffy_tree.layout(root_node).unwrap(); 103 | self.resolve_box(position, transform, result, z_index); 104 | self.resolve_clip(clip_bounds); 105 | 106 | self.finalize_borders(element_state); 107 | } 108 | 109 | fn as_any(&self) -> &dyn Any { 110 | self 111 | } 112 | } 113 | 114 | impl Image { 115 | generate_component_methods_no_children!(); 116 | } 117 | 118 | impl ElementStyles for Image { 119 | fn styles_mut(&mut self) -> &mut Style { 120 | self.element_data.current_style_mut() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/craft_core/src/devtools/tree_window.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{ComponentId, ComponentSpecification}; 2 | use crate::devtools::dev_tools_colors::{ 3 | CONTAINER_BACKGROUND_COLOR, ROW_BACKGROUND_COLOR, SELECTED_ROW_BACKGROUND_COLOR, 4 | }; 5 | use crate::elements::element::Element; 6 | use crate::elements::{Container, ElementStyles, Text}; 7 | use crate::style::{AlignItems, Display, FlexDirection}; 8 | use crate::Color; 9 | use taffy::Overflow; 10 | 11 | pub(crate) fn tree_window( 12 | root_element: &dyn Element, 13 | selected_element: Option, 14 | ) -> ComponentSpecification { 15 | let mut element_tree = Container::new() 16 | .width("100%") 17 | .height("50%") 18 | .overflow(Overflow::Scroll) 19 | .max_height("50%") 20 | .padding("0px", "5px", "5px", "5px") 21 | .flex_direction(FlexDirection::Column); 22 | 23 | let mut elements: Vec<(&dyn Element, usize, bool)> = vec![(root_element, 0, true)]; 24 | let mut element_count = 0; 25 | 26 | while let Some((element, indent, _is_last)) = elements.pop() { 27 | let row_color = if selected_element.is_some() && selected_element.unwrap() == element.component_id() { 28 | SELECTED_ROW_BACKGROUND_COLOR 29 | } else if element_count % 2 == 0 { 30 | ROW_BACKGROUND_COLOR 31 | } else { 32 | CONTAINER_BACKGROUND_COLOR 33 | }; 34 | 35 | let row_name = element.name().to_string(); 36 | 37 | let id = element.component_id().to_string(); 38 | 39 | let mut row = Container::new() 40 | .push( 41 | Text::new(row_name.as_str()) 42 | .padding("0px", "0px", "0px", format!("{}px", indent * 10).as_str()) 43 | .color(Color::WHITE) 44 | .component(), 45 | ) 46 | .display(Display::Flex) 47 | .align_items(AlignItems::Center) 48 | .background(row_color) 49 | .padding("6px", "6px", "6px", "6px") 50 | .height("40px") 51 | .id(format!("tree_view_{}", id.as_str()).as_str()) 52 | .max_height("40px") 53 | .key(element_count.to_string().as_str()) 54 | .width("100%"); 55 | 56 | if let Some(custom_id) = element.get_id() { 57 | let user_id_color = Color::from_rgb8(68, 147, 248); 58 | row = row.push( 59 | Container::new() 60 | .push( 61 | Text::new(custom_id) 62 | .color(Color::WHITE) 63 | .margin("2.5px", "10px", "2.5px", "10px") 64 | ) 65 | .border_width("2px", "2px", "2px", "2px") 66 | .border_color(user_id_color) 67 | .border_radius(100.0, 100.0, 100.0, 100.0) 68 | .margin("0px", "0px", "0px", "5px") 69 | .component(), 70 | ); 71 | } 72 | 73 | let user_id_color = Color::from_rgb8(68, 147, 248); 74 | row = row.push( 75 | Container::new() 76 | .push( 77 | Text::new(element.component_id().to_string().as_str()) 78 | .color(Color::WHITE) 79 | .margin("2.5px", "10px", "2.5px", "10px"), 80 | ) 81 | .border_width("2px", "2px", "2px", "2px") 82 | .border_color(user_id_color) 83 | .border_radius(100.0, 100.0, 100.0, 100.0) 84 | .margin("0px", "0px", "0px", "5px") 85 | .component(), 86 | ); 87 | 88 | element_tree = element_tree.push(row); 89 | 90 | let children = element.children(); 91 | for (i, child) in children.iter().enumerate().rev() { 92 | let is_last = i == children.len() - 1; 93 | elements.push((child.internal.as_ref(), indent + 1, is_last)); 94 | } 95 | 96 | element_count += 1; 97 | } 98 | 99 | element_tree.component() 100 | } -------------------------------------------------------------------------------- /website/src/main.rs: -------------------------------------------------------------------------------- 1 | mod examples; 2 | mod index; 3 | mod web_link; 4 | mod navbar; 5 | mod theme; 6 | mod router; 7 | mod docs; 8 | mod link; 9 | 10 | use crate::index::index_page; 11 | use crate::navbar::Navbar; 12 | use crate::router::resolve_route; 13 | use crate::theme::BODY_BACKGROUND_COLOR; 14 | use craft::components::{Component, ComponentSpecification, Context}; 15 | use craft::elements::Container; 16 | use craft::elements::ElementStyles; 17 | #[cfg(not(target_arch = "wasm32"))] 18 | use craft::geometry::Size; 19 | use craft::style::Display; 20 | use craft::style::FlexDirection; 21 | use craft::{craft_main, CraftOptions}; 22 | 23 | pub(crate) struct WebsiteGlobalState { 24 | /// The current route that we are viewing. 25 | route: String, 26 | } 27 | 28 | impl WebsiteGlobalState { 29 | pub(crate) fn get_route(&self) -> String { 30 | #[cfg(target_arch = "wasm32")] 31 | let path: String; 32 | #[cfg(target_arch = "wasm32")] 33 | { 34 | let window = web_sys::window().expect("No window available."); 35 | path = window.location().pathname().map( 36 | |s| { 37 | let trimmed_path = s.trim_end_matches('/'); 38 | if trimmed_path.is_empty() { 39 | "/".to_string() 40 | } else { 41 | trimmed_path.to_string() 42 | } 43 | } 44 | ).unwrap_or("/".to_string()); 45 | } 46 | #[cfg(not(target_arch = "wasm32"))] 47 | let path = self.route.clone(); 48 | path 49 | } 50 | 51 | pub(crate) fn set_route(&mut self, route: &str) { 52 | self.route = route.to_string(); 53 | 54 | #[cfg(target_arch = "wasm32")] 55 | { 56 | let window = web_sys::window().unwrap(); 57 | let history = window.history().unwrap(); 58 | 59 | history 60 | .push_state_with_url(&web_sys::wasm_bindgen::JsValue::NULL, "", Some(route)) 61 | .unwrap(); 62 | } 63 | } 64 | } 65 | 66 | impl Default for WebsiteGlobalState { 67 | fn default() -> Self { 68 | WebsiteGlobalState { 69 | route: "/".to_string(), 70 | } 71 | } 72 | } 73 | 74 | #[derive(Default)] 75 | pub(crate) struct Website {} 76 | 77 | impl Component for Website { 78 | type GlobalState = WebsiteGlobalState; 79 | type Props = (); 80 | type Message = (); 81 | 82 | fn view(context: &mut Context) -> ComponentSpecification { 83 | let wrapper = Container::new() 84 | .display(Display::Flex) 85 | .flex_direction(FlexDirection::Column) 86 | .width("100%") 87 | .height("100%") 88 | .push(Navbar::component()) 89 | .background(BODY_BACKGROUND_COLOR); 90 | 91 | 92 | let path = context.global_state().get_route(); 93 | let matched_mapped_path = resolve_route(path.as_str(), context.window()); 94 | if let Some(rule) = matched_mapped_path { 95 | wrapper.push(rule.component_specification) 96 | } else { 97 | wrapper.push(index_page(context.window()).key("index")) 98 | }.component() 99 | } 100 | } 101 | 102 | fn main() { 103 | let window_title = "Craft"; 104 | 105 | #[cfg(not(target_arch = "wasm32"))] 106 | let options = CraftOptions { 107 | window_title: window_title.to_string(), 108 | window_size: Some(Size::new(1600.0, 900.0)), 109 | ..Default::default() 110 | }; 111 | 112 | #[cfg(target_arch = "wasm32")] 113 | let options = CraftOptions::basic(window_title); 114 | 115 | #[allow(unused_mut)] 116 | let mut global_state = WebsiteGlobalState::default(); 117 | #[cfg(not(target_arch = "wasm32"))] 118 | { 119 | // NOTE: In Git Bash, use `cargo run -- //examples`. 120 | let route = std::env::args().nth(1).unwrap_or_else(|| "/".to_string()); 121 | global_state.set_route(route.as_str()); 122 | } 123 | 124 | util::setup_logging(); 125 | 126 | craft_main(Website::component(), global_state, options); 127 | } 128 | -------------------------------------------------------------------------------- /examples/events/main.rs: -------------------------------------------------------------------------------- 1 | use craft::components::{Component, ComponentSpecification}; 2 | use craft::components::Context; 3 | use craft::elements::Container; 4 | use craft::elements::ElementStyles; 5 | use craft::palette; 6 | use craft::style::{BoxSizing, Display, FlexDirection, Overflow}; 7 | use craft::CraftOptions; 8 | use craft::{craft_main, Color}; 9 | use util::setup_logging; 10 | 11 | #[derive(Default, Copy, Clone)] 12 | pub struct EventsExample {} 13 | 14 | impl Component for EventsExample { 15 | type GlobalState = (); 16 | 17 | type Props = (); 18 | 19 | type Message = (); 20 | 21 | fn view(_context: &mut Context) -> ComponentSpecification { 22 | let scroll_example = Container::new() 23 | .display(Display::Block) 24 | .width("200px") 25 | .height("100px") 26 | .overflow_y(Overflow::Scroll) 27 | .border_width(4, 4, 4, 4) 28 | .border_color(Color::BLACK) 29 | .push( 30 | Container::new() 31 | .height("300px") 32 | .display(Display::Block) 33 | .background(Color::from_rgb8(170, 170, 255)) 34 | .component(), 35 | ) 36 | .component(); 37 | 38 | let padded_scroll = Container::new() 39 | .display(Display::Block) 40 | .width("200px") 41 | .height("100px") 42 | .padding("20px", "20px", "20px", "20px") 43 | .overflow_y(Overflow::Scroll) 44 | .border_width(10, 10, 10, 10) 45 | .border_color(Color::BLACK) 46 | .box_sizing(BoxSizing::BorderBox) 47 | .push( 48 | Container::new().height("300px").display(Display::Block).background(palette::css::LAVENDER).component(), 49 | ) 50 | .component(); 51 | 52 | let nested_scroll = Container::new() 53 | .width("300px") 54 | .height("150px") 55 | .display(Display::Block) 56 | .overflow_y(Overflow::Scroll) 57 | .padding(20, 20, 20, 20) 58 | .border_width(2, 2, 2, 2) 59 | .border_color(palette::css::PURPLE) 60 | .push( 61 | Container::new() 62 | .display(Display::Block) 63 | .width("220px") 64 | .height("300px") 65 | .overflow_y(Overflow::Scroll) 66 | .border_width(4, 4, 4, 4) 67 | .border_color(palette::css::GOLD) 68 | .padding(10, 10, 10, 10) 69 | .background(palette::css::GREEN) 70 | .push( 71 | Container::new() 72 | .display(Display::Block) 73 | .width("150px") 74 | .height("900px") 75 | .background(palette::css::BLUE) 76 | .component(), 77 | ) 78 | .component(), 79 | ) 80 | .component(); 81 | 82 | Container::new() 83 | .display(Display::Flex) 84 | .flex_direction(FlexDirection::Column) 85 | // .overflow_y(Overflow::Scroll) 86 | .height("100%") 87 | .max_height("100%") 88 | .gap("50px") 89 | .push(scroll_example) 90 | .push(padded_scroll) 91 | .push(nested_scroll) 92 | .component() 93 | } 94 | fn update(context: &mut Context) { 95 | if context.message().clicked() { 96 | let target = if let Some(target) = context.target() { target.get_id().clone() } else { None }; 97 | let current_target = 98 | if let Some(current_target) = context.current_target() { current_target.get_id().clone() } else { None }; 99 | println!("Target: {:?}, Current Target: {:?}", target, current_target); 100 | } 101 | } 102 | } 103 | 104 | fn main() { 105 | setup_logging(); 106 | craft_main(EventsExample::component(), (), CraftOptions::basic("Events")); 107 | } 108 | -------------------------------------------------------------------------------- /crates/craft_core/src/reactive/fiber_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::element::Element; 2 | use crate::elements::{Dropdown, Overlay}; 3 | use crate::reactive::tree::ComponentTreeNode; 4 | use std::cell::RefCell; 5 | use std::rc::{Rc, Weak}; 6 | use smallvec::SmallVec; 7 | 8 | #[derive(Clone)] 9 | /// Links the ComponentTree with the ElementTree. 10 | /// 11 | /// This is needed because the component tree does not have a reference to the element tree. 12 | pub(crate) struct FiberNode<'a> { 13 | /// The component tree node. Both "Components" and "Elements" are components. 14 | pub(crate) component: &'a ComponentTreeNode, 15 | /// If the node is an element, this will be Some(element). 16 | pub(crate) element: Option<&'a dyn Element>, 17 | /// The children of this node. This is a vector of FiberNode. 18 | pub(crate) children: SmallVec<[Rc>>; 4]>, 19 | pub(crate) parent: Option>>>, 20 | pub(crate) overlay_order: u32, 21 | } 22 | 23 | pub fn new<'a>(root_component: &'a ComponentTreeNode, root_element: &'a dyn Element) -> Rc>> { 24 | // Dummy lets us treat the real root like any other child. 25 | let dummy_root = Rc::new(RefCell::new(FiberNode { 26 | component: root_component, 27 | element: None, 28 | children: SmallVec::new(), 29 | parent: None, 30 | overlay_order: 0, 31 | })); 32 | 33 | // ┌──────────────────┬────────────────────────┬──────────────────┐ 34 | // │ component_stack │ parent_fiber_stack │ element_stack │ 35 | // └──────────────────┴────────────────────────┴──────────────────┘ 36 | let mut component_stack: Vec<&'a ComponentTreeNode> = vec![root_component]; 37 | let mut parent_fiber_stack: Vec>> = vec![Rc::clone(&dummy_root)]; 38 | let mut element_stack: Vec<&'a dyn Element> = vec![root_element]; 39 | 40 | while let (Some(component), Some(parent_fiber)) = (component_stack.pop(), parent_fiber_stack.pop()) { 41 | let mut overlay_order = parent_fiber.borrow().overlay_order; 42 | // If the component *is* an element, pop the matching element and 43 | // push its children so the two stacks stay aligned. 44 | let element = if component.is_element { 45 | let element = element_stack.pop().expect("component / element stacks out of sync"); 46 | if element.as_any().is::() || element.as_any().is::() { 47 | overlay_order += 1; 48 | } 49 | for child_element in element.children().iter().rev() { 50 | element_stack.push(child_element.internal.as_ref()); 51 | } 52 | Some(element) 53 | } else { 54 | None 55 | }; 56 | 57 | // Build the real fiber node and attach it to its parent. 58 | let this_fiber = Rc::new(RefCell::new(FiberNode { 59 | component, 60 | element, 61 | children: SmallVec::new(), 62 | parent: Some(Rc::downgrade(&parent_fiber)), 63 | overlay_order, 64 | })); 65 | parent_fiber.borrow_mut().children.push(Rc::clone(&this_fiber)); 66 | 67 | // Sanity-check: the ID stored in the component must really point 68 | // to the parent we just attached it to. 69 | if component.id != 0 { 70 | debug_assert_eq!( 71 | component.parent_id, 72 | Some(parent_fiber.borrow().component.id), 73 | "component {} expects parent {:?}, but actual parent is {}", 74 | component.id, 75 | component.parent_id, 76 | parent_fiber.borrow().component.id, 77 | ); 78 | } 79 | 80 | // Queue the component’s children and remember *this* fiber as 81 | // their parent. 82 | for child in component.children.iter().rev() { 83 | component_stack.push(child); 84 | parent_fiber_stack.push(Rc::clone(&this_fiber)); 85 | } 86 | } 87 | 88 | // The dummy now has exactly one child: the tree’s true root. 89 | let root = Rc::clone(&dummy_root.borrow().children.first().expect("component tree was empty")); 90 | root.borrow_mut().parent = None; 91 | 92 | root 93 | } 94 | -------------------------------------------------------------------------------- /crates/craft_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "craft_core" 3 | description = "Core library for the Craft GUI framework." 4 | version = "0.1.1" 5 | edition = "2024" 6 | resolver = "2" 7 | license-file = "LICENSE" 8 | homepage = "https://craftgui.com/" 9 | repository = "https://github.com/craft-gui/craft" 10 | 11 | [features] 12 | dev_tools = [] 13 | dynamic_linking = [] 14 | clipboard = ["dep:clipboard-rs"] 15 | 16 | system_fonts = ["parley/system"] 17 | 18 | png = ["image/png"] 19 | jpeg = ["image/jpeg"] 20 | 21 | accesskit = ["dep:accesskit", "dep:accesskit_winit", "parley/accesskit"] 22 | 23 | markdown = ["dep:pulldown-cmark", "code_highlighting", "link"] 24 | code_highlighting = ["dep:syntect"] 25 | link = ["dep:open"] 26 | 27 | default = ["clipboard", "accesskit"] 28 | 29 | 30 | [dependencies] 31 | craft_logging = { path = "../craft_logger", version = "0.1.0" } 32 | 33 | smol_str = "0.3.2" 34 | 35 | [dependencies.cfg-if] 36 | workspace = true 37 | 38 | [dependencies.accesskit_winit] 39 | version = "0.27.0" 40 | default-features = false 41 | features = ["tokio", "rwh_06", "accesskit_unix"] 42 | optional = true 43 | 44 | [dependencies.accesskit] 45 | version = "0.19.0" 46 | default-features = false 47 | optional = true 48 | 49 | [dependencies.bitflags] 50 | version = "2.9.1" 51 | features = ["std"] 52 | 53 | [dependencies.kurbo] 54 | workspace = true 55 | 56 | [dependencies.smallvec] 57 | version = "1.15.1" 58 | default-features = false 59 | 60 | [dependencies.peniko] 61 | workspace = true 62 | 63 | [dependencies.tinyvg-rs] 64 | workspace = true 65 | 66 | [dependencies.rustc-hash] 67 | version = "2.1.1" 68 | default-features = false 69 | features = ["std"] 70 | 71 | [target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies.clipboard-rs] 72 | version = "0.2.4" 73 | optional = true 74 | 75 | [dependencies.pulldown-cmark] 76 | version = "0.13.0" 77 | default-features = false 78 | features = [] 79 | optional = true 80 | 81 | [dependencies.syntect] 82 | version = "5.2" 83 | default-features = false 84 | features = ["default-fancy", "parsing", "dump-load", "default-themes"] 85 | optional = true 86 | 87 | [dependencies.image] 88 | workspace = true 89 | 90 | [dependencies.taffy] 91 | version = "0.8.2" 92 | default-features = false 93 | features = ["std", "taffy_tree", "flexbox", "content_size", "block_layout"] 94 | 95 | [dependencies.chrono] 96 | workspace = true 97 | 98 | [target.'cfg(not(target_os = "android"))'.dependencies.winit] 99 | workspace = true 100 | features = [] 101 | 102 | [target.'cfg(target_os = "android")'.dependencies.winit] 103 | workspace = true 104 | features = ["android-native-activity"] 105 | 106 | [dependencies.ui-events] 107 | git = "https://github.com/AustinMReppert/ui-events" 108 | branch = "experiment" 109 | 110 | [dependencies.ui-events-winit] 111 | git = "https://github.com/AustinMReppert/ui-events" 112 | branch = "experiment" 113 | 114 | [dependencies.parley] 115 | version = "0.5.0" 116 | default-features = false 117 | features = ["std"] 118 | 119 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-time] 120 | version = "1.1.0" 121 | default-features = false 122 | features = [] 123 | 124 | [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] 125 | version = "0.2.100" 126 | default-features = false 127 | features = ["std", "msrv"] 128 | 129 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 130 | version = "0.3.77" 131 | default-features = false 132 | features = [ 133 | "Document", 134 | "Window", 135 | "Element", 136 | ] 137 | 138 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.open] 139 | version = "5.3.2" 140 | default-features = false 141 | optional = true 142 | features = [] 143 | 144 | [dependencies.craft_primitives] 145 | path = "../craft_primitives" 146 | default-features = false 147 | version = "0.1.1" 148 | 149 | [dependencies.craft_renderer] 150 | path = "../craft_renderer" 151 | default-features = false 152 | version = "0.1.1" 153 | 154 | [dependencies.craft_runtime] 155 | path = "../craft_runtime" 156 | default-features = false 157 | version = "0.1.1" 158 | 159 | [dependencies.craft_resource_manager] 160 | path = "../craft_resource_manager" 161 | default-features = false 162 | version = "0.1.1" -------------------------------------------------------------------------------- /crates/craft_core/src/elements/thumb.rs: -------------------------------------------------------------------------------- 1 | use kurbo::Affine; 2 | use craft_primitives::geometry::{Point, Rectangle}; 3 | use crate::layout::layout_context::LayoutContext; 4 | use crate::layout::layout_item::LayoutItem; 5 | use crate::palette; 6 | use crate::reactive::element_state_store::ElementStateStore; 7 | use craft_renderer::renderer::RenderList; 8 | use crate::style::{Display, Style, Unit}; 9 | use crate::text::text_context::TextContext; 10 | use taffy::{NodeId, Position, TaffyTree}; 11 | 12 | #[derive(Clone)] 13 | pub(crate) struct Thumb { 14 | /// A pseudo thumb element, this is not stored in the user tree nor will it receive events. 15 | /// This is mostly for convenience, so that we can change the location and render it in the switch track container. 16 | pub(crate) layout_item: LayoutItem, 17 | /// The style of the thumb when the switch is toggled. This style will get merged with the default style + user style. 18 | pub(crate) thumb_style: Style, 19 | pub(crate) toggled_thumb_style: Style, 20 | /// The size of the thumb in pixels. 21 | pub(crate) size: f32, 22 | } 23 | 24 | impl Thumb { 25 | pub(crate) fn default_thumb_style(&self, rounded: bool) -> Style { 26 | let mut style = Style::default(); 27 | 28 | style.set_display(Display::Block); 29 | style.set_width(Unit::Px(self.size)); 30 | style.set_height(Unit::Px(self.size)); 31 | style.set_background(palette::css::WHITE); 32 | style.set_position(Position::Relative); 33 | 34 | if rounded { 35 | let rounding = self.size / 2.0; 36 | style.set_border_radius([ 37 | (rounding, rounding), 38 | (rounding, rounding), 39 | (rounding, rounding), 40 | (rounding, rounding), 41 | ]); 42 | } 43 | 44 | style 45 | } 46 | 47 | pub(crate) fn default_toggled_thumb_style(&self, rounded: bool) -> Style { 48 | let style = Style::default(); 49 | Style::merge(&self.default_thumb_style(rounded), &style) 50 | } 51 | 52 | pub(crate) fn thumb_style(&mut self, thumb_style: Style) { 53 | self.thumb_style = thumb_style; 54 | } 55 | 56 | pub(crate) fn toggled_thumb_style(&mut self, toggled_thumb_style: Style) { 57 | self.toggled_thumb_style = toggled_thumb_style; 58 | } 59 | 60 | pub(crate) fn compute_layout( 61 | &mut self, 62 | taffy_tree: &mut TaffyTree, 63 | _scale_factor: f64, 64 | toggled: bool, 65 | rounded: bool, 66 | ) -> NodeId { 67 | self.thumb_style = Style::merge(&self.default_thumb_style(rounded), &self.thumb_style); 68 | 69 | if toggled { 70 | self.thumb_style = Style::merge(&self.thumb_style, &self.default_toggled_thumb_style(rounded)); 71 | self.thumb_style = Style::merge(&self.thumb_style, &self.toggled_thumb_style); 72 | } 73 | 74 | self.layout_item.build_tree(taffy_tree, self.thumb_style.to_taffy_style()).unwrap() 75 | } 76 | 77 | #[allow(clippy::too_many_arguments)] 78 | pub(crate) fn finalize_layout( 79 | &mut self, 80 | taffy_tree: &mut TaffyTree, 81 | position: Point, 82 | z_index: &mut u32, 83 | transform: Affine, 84 | _element_state: &mut ElementStateStore, 85 | _pointer: Option, 86 | _text_context: &mut TextContext, 87 | clip_bounds: Option, 88 | ) { 89 | let result = taffy_tree.layout(self.layout_item.taffy_node_id.unwrap()).unwrap(); 90 | self.layout_item.resolve_box(position, transform, result, z_index, self.thumb_style.position()); 91 | self.layout_item.finalize_borders( 92 | self.thumb_style.has_border(), 93 | self.thumb_style.border_radius(), 94 | self.thumb_style.border_color(), 95 | ); 96 | self.layout_item.resolve_clip(clip_bounds); 97 | } 98 | 99 | pub(crate) fn draw(&mut self, renderer: &mut RenderList, scale_factor: f64) { 100 | if !self.thumb_style.visible() { 101 | return; 102 | } 103 | 104 | self.layout_item.draw_borders(renderer, &self.thumb_style, scale_factor); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/tinyvg.rs: -------------------------------------------------------------------------------- 1 | use crate::components::component::ComponentSpecification; 2 | use crate::components::Props; 3 | use crate::elements::element::Element; 4 | use crate::elements::element_data::ElementData; 5 | use crate::elements::ElementStyles; 6 | use crate::generate_component_methods_no_children; 7 | use craft_primitives::geometry::{Point, Rectangle}; 8 | use crate::layout::layout_context::{LayoutContext, TinyVgContext}; 9 | use crate::reactive::element_state_store::ElementStateStore; 10 | use craft_renderer::renderer::RenderList; 11 | use craft_resource_manager::ResourceIdentifier; 12 | use crate::style::Style; 13 | use crate::text::text_context::TextContext; 14 | use peniko::Color; 15 | use std::any::Any; 16 | use std::sync::Arc; 17 | use kurbo::Affine; 18 | use taffy::{NodeId, TaffyTree}; 19 | use winit::window::Window; 20 | use smol_str::SmolStr; 21 | 22 | #[derive(Clone)] 23 | pub struct TinyVg { 24 | pub(crate) resource_identifier: ResourceIdentifier, 25 | pub element_data: ElementData, 26 | } 27 | 28 | impl TinyVg { 29 | pub fn new(resource_identifier: ResourceIdentifier) -> TinyVg { 30 | TinyVg { 31 | resource_identifier, 32 | element_data: Default::default(), 33 | } 34 | } 35 | 36 | pub fn name() -> &'static str { 37 | "TinyVG" 38 | } 39 | } 40 | 41 | impl Element for TinyVg { 42 | fn element_data(&self) -> &ElementData { 43 | &self.element_data 44 | } 45 | 46 | fn element_data_mut(&mut self) -> &mut ElementData { 47 | &mut self.element_data 48 | } 49 | 50 | fn name(&self) -> &'static str { 51 | "TinyVG" 52 | } 53 | 54 | fn draw( 55 | &mut self, 56 | renderer: &mut RenderList, 57 | _text_context: &mut TextContext, 58 | element_state: &mut ElementStateStore, 59 | _pointer: Option, 60 | _window: Option>, 61 | scale_factor: f64, 62 | ) { 63 | if !self.element_data.style.visible() { 64 | return; 65 | } 66 | let computed_box_transformed = self.computed_box_transformed(); 67 | let content_rectangle = computed_box_transformed.content_rectangle(); 68 | self.draw_borders(renderer, element_state, scale_factor); 69 | 70 | let mut color = None; 71 | if self.style().color() != Color::TRANSPARENT { 72 | color = Some(self.style().color()); 73 | } 74 | renderer.draw_tiny_vg(content_rectangle.scale(scale_factor), self.resource_identifier.clone(), color); 75 | } 76 | 77 | fn compute_layout( 78 | &mut self, 79 | taffy_tree: &mut TaffyTree, 80 | _element_state: &mut ElementStateStore, 81 | _scale_factor: f64, 82 | ) -> Option { 83 | self.merge_default_style(); 84 | let style: taffy::Style = self.element_data.style.to_taffy_style(); 85 | 86 | self.element_data.layout_item.build_tree_with_context( 87 | taffy_tree, 88 | style, 89 | LayoutContext::TinyVg(TinyVgContext { 90 | resource_identifier: self.resource_identifier.clone(), 91 | }), 92 | ) 93 | } 94 | 95 | fn finalize_layout( 96 | &mut self, 97 | taffy_tree: &mut TaffyTree, 98 | root_node: NodeId, 99 | position: Point, 100 | z_index: &mut u32, 101 | transform: Affine, 102 | element_state: &mut ElementStateStore, 103 | _pointer: Option, 104 | _text_context: &mut TextContext, 105 | clip_bounds: Option, 106 | ) { 107 | let result = taffy_tree.layout(root_node).unwrap(); 108 | self.resolve_box(position, transform, result, z_index); 109 | 110 | self.finalize_borders(element_state); 111 | self.resolve_clip(clip_bounds); 112 | } 113 | 114 | fn as_any(&self) -> &dyn Any { 115 | self 116 | } 117 | 118 | fn default_style(&self) -> Style { 119 | let mut style = Style::default(); 120 | style.set_color(Color::TRANSPARENT); 121 | style 122 | } 123 | } 124 | 125 | impl TinyVg { 126 | generate_component_methods_no_children!(); 127 | } 128 | 129 | impl ElementStyles for TinyVg { 130 | fn styles_mut(&mut self) -> &mut Style { 131 | self.element_data.current_style_mut() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /crates/craft_primitives/src/geometry/element_box.rs: -------------------------------------------------------------------------------- 1 | use kurbo::Affine; 2 | use crate::geometry::{Border, Margin, Padding, Point, Rectangle, Size}; 3 | 4 | #[derive(Clone, Copy, Debug, Default)] 5 | pub struct ElementBox { 6 | pub margin: Margin, 7 | pub border: Border, 8 | pub padding: Padding, 9 | pub position: Point, 10 | pub size: Size, 11 | } 12 | 13 | impl ElementBox { 14 | pub fn transform(&self, transform: Affine) -> Self { 15 | let mut transformed_box = *self; 16 | transformed_box.position = transform * self.position; 17 | transformed_box 18 | } 19 | 20 | pub fn margin_rectangle_position(&self) -> Point { 21 | Point::new(self.position.x - self.margin.left as f64, self.position.y - self.margin.top as f64) 22 | } 23 | 24 | pub fn margin_rectangle_size(&self) -> Size { 25 | let margin_width = self.size.width + self.margin.left + self.margin.right; 26 | let margin_height = self.size.height + self.margin.top + self.margin.bottom; 27 | Size { 28 | width: margin_width, 29 | height: margin_height, 30 | } 31 | } 32 | 33 | pub fn margin_rectangle(&self) -> Rectangle { 34 | let margin_position = self.margin_rectangle_position(); 35 | let margin_size = self.margin_rectangle_size(); 36 | 37 | Rectangle { 38 | x: margin_position.x as f32, 39 | y: margin_position.y as f32, 40 | width: margin_size.width, 41 | height: margin_size.height, 42 | } 43 | } 44 | 45 | pub fn border_rectangle_size(&self) -> Size { 46 | Size { 47 | width: self.size.width, 48 | height: self.size.height, 49 | } 50 | } 51 | 52 | pub fn border_rectangle_position(&self) -> Point { 53 | Point::new(self.position.x, self.position.y) 54 | } 55 | 56 | pub fn border_rectangle(&self) -> Rectangle { 57 | let border_position = self.border_rectangle_position(); 58 | let border_size = self.border_rectangle_size(); 59 | 60 | Rectangle { 61 | x: border_position.x as f32, 62 | y: border_position.y as f32, 63 | width: border_size.width, 64 | height: border_size.height, 65 | } 66 | } 67 | 68 | pub fn padding_rectangle_size(&self) -> Size { 69 | let padding_width = self.size.width - self.border.left - self.border.right; 70 | let padding_height = self.size.height - self.border.top - self.border.bottom; 71 | Size { 72 | width: padding_width, 73 | height: padding_height, 74 | } 75 | } 76 | 77 | pub fn padding_rectangle_position(&self) -> Point { 78 | let padding_x = self.position.x + self.border.left as f64; 79 | let padding_y = self.position.y + self.border.top as f64; 80 | Point::new(padding_x, padding_y) 81 | } 82 | 83 | pub fn padding_rectangle(&self) -> Rectangle { 84 | let padding_position = self.padding_rectangle_position(); 85 | let padding_size = self.padding_rectangle_size(); 86 | 87 | Rectangle { 88 | x: padding_position.x as f32, 89 | y: padding_position.y as f32, 90 | width: padding_size.width, 91 | height: padding_size.height, 92 | } 93 | } 94 | 95 | pub fn content_rectangle_size(&self) -> Size { 96 | let content_width = 97 | self.size.width - self.padding.left - self.padding.right - self.border.left - self.border.right; 98 | let content_height = 99 | self.size.height - self.padding.top - self.padding.bottom - self.border.top - self.border.bottom; 100 | Size::new(content_width, content_height) 101 | } 102 | 103 | pub fn content_rectangle_position(&self) -> Point { 104 | let content_x = self.position.x as f32 + self.border.left + self.padding.left; 105 | let content_y = self.position.y as f32 + self.border.top + self.padding.top; 106 | Point::new(content_x as f64, content_y as f64) 107 | } 108 | 109 | pub fn content_rectangle(&self) -> Rectangle { 110 | let content_position = self.content_rectangle_position(); 111 | let content_size = self.content_rectangle_size(); 112 | 113 | Rectangle::new(content_position.x as f32, content_position.y as f32, content_size.width, content_size.height) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/craft_core/src/components/update_result.rs: -------------------------------------------------------------------------------- 1 | use crate::components::ComponentId; 2 | use crate::events::{CraftMessage, EventDispatchType, Message}; 3 | use craft_primitives::geometry::Rectangle; 4 | use crate::PinnedFutureAny; 5 | use std::any::Any; 6 | use crate::utils::cloneable_any::CloneableAny; 7 | 8 | #[derive(Debug, Clone, Copy, Default)] 9 | pub enum PointerCapture { 10 | #[default] 11 | None, 12 | Set, 13 | Unset, 14 | } 15 | 16 | /// The result of an update. 17 | pub struct Event { 18 | /// Propagate craft_events to the next element. True by default. 19 | pub propagate: bool, 20 | /// A future that will produce a message when complete. The message will be sent to the origin component. 21 | pub future: Option, 22 | /// Prevent default event handlers from running when an craft_event is not explicitly handled. 23 | /// False by default. 24 | pub prevent_defaults: bool, 25 | pub(crate) result_message: Option, 26 | /// Redirect future pointer events to this component. None by default. 27 | pub(crate) pointer_capture: PointerCapture, 28 | pub(crate) effects: Vec<(EventDispatchType, Message)>, 29 | pub(crate) ime: ImeAction, 30 | pub focus: FocusAction, 31 | } 32 | 33 | #[derive(Debug, Clone, Copy, Default)] 34 | pub enum ImeAction { 35 | #[default] 36 | None, 37 | Set(Rectangle), 38 | Unset, 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, Default)] 42 | pub enum FocusAction { 43 | #[default] 44 | None, 45 | Set(ComponentId), 46 | Unset, 47 | } 48 | 49 | impl FocusAction { 50 | 51 | pub(crate) fn merge(&self, other: FocusAction) -> FocusAction { 52 | match other { 53 | FocusAction::None => *self, 54 | FocusAction::Set(id) => FocusAction::Set(id), 55 | FocusAction::Unset => FocusAction::Unset, 56 | } 57 | } 58 | 59 | } 60 | 61 | impl Event { 62 | 63 | #[cfg(not(target_arch = "wasm32"))] 64 | pub fn async_result(t: T) -> Box { 65 | Box::new(t) 66 | } 67 | 68 | #[cfg(target_arch = "wasm32")] 69 | pub fn async_result(t: T) -> Box { 70 | Box::new(t) 71 | } 72 | 73 | #[cfg(not(target_arch = "wasm32"))] 74 | pub fn async_no_result() -> Box { 75 | Box::new(()) 76 | } 77 | 78 | #[cfg(target_arch = "wasm32")] 79 | pub fn async_no_result() -> Box { 80 | Box::new(()) 81 | } 82 | 83 | pub fn ime_action(&mut self, action: ImeAction) { 84 | self.ime = action; 85 | } 86 | 87 | pub fn focus_action(&mut self, action: FocusAction) { 88 | self.focus = action; 89 | } 90 | } 91 | 92 | impl Default for Event { 93 | fn default() -> Self { 94 | Event { 95 | propagate: true, 96 | future: None, 97 | prevent_defaults: false, 98 | result_message: None, 99 | pointer_capture: Default::default(), 100 | effects: Vec::new(), 101 | ime: ImeAction::None, 102 | focus: FocusAction::None, 103 | } 104 | } 105 | } 106 | 107 | impl Event { 108 | pub fn new() -> Event { 109 | Event::default() 110 | } 111 | 112 | pub fn pinned_future(&mut self, future: PinnedFutureAny) { 113 | self.future = Some(future); 114 | } 115 | 116 | #[cfg(not(target_arch = "wasm32"))] 117 | pub fn future> + 'static + Send>(&mut self, future: F) { 118 | self.future = Some(Box::pin(future)); 119 | } 120 | 121 | #[cfg(target_arch = "wasm32")] 122 | pub fn future> + 'static>(&mut self, future: F) { 123 | self.future = Some(Box::pin(future)); 124 | } 125 | 126 | pub fn prevent_defaults(&mut self) { 127 | self.prevent_defaults = true; 128 | } 129 | 130 | pub fn prevent_propagate(&mut self) { 131 | self.propagate = false; 132 | } 133 | 134 | pub fn result_message(&mut self, message: CraftMessage) { 135 | self.result_message = Some(message); 136 | } 137 | 138 | pub fn pointer_capture(&mut self, pointer_capture: PointerCapture) { 139 | self.pointer_capture = pointer_capture; 140 | } 141 | 142 | pub fn add_effect(&mut self, event_dispatch_type: EventDispatchType, message: Message) { 143 | self.effects.push((event_dispatch_type, message)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/craft_core/src/elements/element_data.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | use smol_str::SmolStr; 3 | use crate::components::{ComponentId, ComponentSpecification}; 4 | use crate::components::Props; 5 | use crate::elements::element::ElementBoxed; 6 | use crate::elements::element_states::ElementState; 7 | use crate::events::event_handlers::EventHandlers; 8 | use crate::layout::layout_item::LayoutItem; 9 | use crate::style::Style; 10 | 11 | #[derive(Clone, Default)] 12 | pub struct ElementData { 13 | pub current_state: ElementState, 14 | 15 | /// The style of the element. 16 | pub style: Style, 17 | 18 | pub layout_item: LayoutItem, 19 | 20 | /// The style of the element when it is hovered. 21 | pub hover_style: Option