diff --git a/Cargo.lock b/Cargo.lock index 0a05221f..5ed88dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3909,6 +3909,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-native-tls", @@ -5470,6 +5472,23 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "website" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "craft_retained", + "open", + "reqwest 0.13.2", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", + "tracing-web", + "util", + "web-sys", +] + [[package]] name = "weezl" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 8dad7f12..435fdb17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/craft_resource_manager", "crates/craft_runtime", "crates/craft_undo", + "website", ] [workspace.package] diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index ab6fa18c..ac5e0967 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -36,6 +36,12 @@ impl Default for Container { impl Element for Container {} +impl Drop for ContainerInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Container { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/elements/dropdown.rs b/crates/craft_retained/src/elements/dropdown.rs index b810a871..4aab5daf 100644 --- a/crates/craft_retained/src/elements/dropdown.rs +++ b/crates/craft_retained/src/elements/dropdown.rs @@ -80,6 +80,12 @@ impl Default for Dropdown { impl Element for Dropdown {} +impl Drop for DropdownInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Dropdown { fn as_element_rc(&self) -> Rc> { self.inner.clone() @@ -582,7 +588,7 @@ impl DropdownInner { // Remove the old selected element from the layout tree. if let Some(old_selected_element) = &self.selected_element { TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.remove_subtree(old_selected_element.borrow().element_data().layout.taffy_node_id()); + taffy_tree.unparent_node(old_selected_element.borrow().element_data().layout.taffy_node_id()); }); } @@ -609,7 +615,12 @@ impl DropdownInner { }); } - fn update_most_recently_hovered_child(&mut self, message: &EventKind, list_box: Rectangle, list_scroll_box: Rectangle) { + fn update_most_recently_hovered_child( + &mut self, + message: &EventKind, + list_box: Rectangle, + list_scroll_box: Rectangle, + ) { if let EventKind::PointerMovedEvent(pb) = message { let pointer_position = Point::new(pb.current.position.x, pb.current.position.y); let is_pointer_in_list = list_box.contains(&pointer_position); @@ -656,7 +667,13 @@ impl DropdownInner { } } - fn handle_child_click(&mut self, event: &mut Event, pointer_position: &Point, is_pointer_in_window: bool, is_pointer_in_scrollbar: bool) { + fn handle_child_click( + &mut self, + event: &mut Event, + pointer_position: &Point, + is_pointer_in_window: bool, + is_pointer_in_scrollbar: bool, + ) { if is_pointer_in_window && !is_pointer_in_scrollbar { let mut should_hide_window = false; for (child_index, child) in self.children().iter().cloned().enumerate() { diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 19de6ad2..8a573206 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -47,6 +47,12 @@ impl crate::elements::ElementData for ImageInner { impl Element for Image {} +impl Drop for ImageInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Image { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/elements/slider/slider_element.rs b/crates/craft_retained/src/elements/slider/slider_element.rs index da28520f..d06b1caa 100644 --- a/crates/craft_retained/src/elements/slider/slider_element.rs +++ b/crates/craft_retained/src/elements/slider/slider_element.rs @@ -305,6 +305,12 @@ impl SliderInner { impl Element for Slider {} +impl Drop for SliderInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Slider { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 70e7b083..52fe90f3 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -89,6 +89,12 @@ pub struct TextState { impl Element for Text {} +impl Drop for TextInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Text { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index 2cf2fd5c..f9e1a420 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -135,6 +135,12 @@ impl TextInput { impl Element for TextInput {} +impl Drop for TextInputInner { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for TextInput { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/elements/traits/element_internals.rs b/crates/craft_retained/src/elements/traits/element_internals.rs index 0029f722..a6991e39 100644 --- a/crates/craft_retained/src/elements/traits/element_internals.rs +++ b/crates/craft_retained/src/elements/traits/element_internals.rs @@ -14,7 +14,7 @@ use craft_renderer::RenderList; use crate::app::{ELEMENTS, FOCUS, TAFFY_TREE}; use crate::elements::scrollable::{ScrollState, draw_scrollbar}; -use crate::elements::{ElementData, ElementIdMap, ScrollOptions, WindowInternal}; +use crate::elements::{ElementData, ScrollOptions, WindowInternal}; use crate::events::pointer_capture::PointerCapture; use crate::events::{DropdownItemSelectedHandler, Event, EventKind, KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, ScrollHandler, SliderValueChangedHandler}; use crate::layout::TaffyTree; @@ -23,7 +23,10 @@ use crate::text::text_context::TextContext; use crate::{Color, CraftError}; /// Internal element methods that should typically be ignored by users. Public for custom elements. -pub trait ElementInternals: ElementData + Any { +/// +/// Drop is required to clean up any taffy nodes allocated by the element. +#[allow(drop_bounds)] +pub trait ElementInternals: ElementData + Any + Drop { fn deep_clone(&self) -> Rc>; fn position_in_parent(&self) -> Option { @@ -444,14 +447,14 @@ pub trait ElementInternals: ElementData + Any { // Remove the parent reference. child.borrow_mut().element_data_mut().parent = None; - child.borrow_mut().element_data_mut().window = None; + //child.borrow_mut().element_data_mut().window = None; child.borrow_mut().propagate_window_down(); TAFFY_TREE.with_borrow_mut(|taffy_tree| { let child_id = child.borrow().element_data().layout.taffy_node_id; if let Some(child_id) = child_id { - taffy_tree.remove_subtree(child_id); + taffy_tree.unparent_node(child_id); } let parent_id = self.element_data().layout.taffy_node_id; @@ -459,21 +462,14 @@ pub trait ElementInternals: ElementData + Any { }); // TODO: Move to document - fn remove_element_from_document( - node: Rc>, - pointer_capture: &mut PointerCapture, - elements: &mut ElementIdMap, - ) { - elements.remove_id(node.borrow().element_data().internal_id); + fn remove_element_from_document(node: Rc>, pointer_capture: &mut PointerCapture) { pointer_capture.remove_element(&node); for child in node.borrow().children() { - remove_element_from_document(child.clone(), pointer_capture, elements); + remove_element_from_document(child.clone(), pointer_capture); } } - ELEMENTS.with_borrow_mut(|elements| { - remove_element_from_document(child.clone(), &mut self.pointer_capture().borrow_mut(), elements); - }); + remove_element_from_document(child.clone(), &mut self.pointer_capture().borrow_mut()); child.borrow_mut().unfocus(); @@ -938,6 +934,32 @@ pub trait ElementInternals: ElementData + Any { .winit_window .clone() } + + /// Recursively prints the IDs of this element and all of its descendants. + fn print_tree_ids(&self, depth: usize) { + let indent = " ".repeat(depth); + + // Access the ID from element_data. + // If it's None, we can print "Unnamed Element" or the internal_id. + let id_label = self.element_data().internal_id.to_string(); + + println!("{}└─ {}: {}", indent, id_label, self.element_data().window.is_some()); + + for child in self.children() { + child.borrow().print_tree_ids(depth + 1); + } + } + + fn drop(&mut self) { + if let Some(taffy_node) = self.element_data().layout.taffy_node_id { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.remove_node(taffy_node); + }); + } + ELEMENTS.with_borrow_mut(|elements| { + elements.remove_id(self.element_data().internal_id); + }); + } } pub fn resolve_clip_for_scrollable(element: &mut dyn ElementInternals, clip_bounds: Option) { diff --git a/crates/craft_retained/src/elements/window.rs b/crates/craft_retained/src/elements/window.rs index 1129c730..42ffab8c 100644 --- a/crates/craft_retained/src/elements/window.rs +++ b/crates/craft_retained/src/elements/window.rs @@ -110,6 +110,12 @@ impl Default for Window { impl Element for Window {} +impl Drop for WindowInternal { + fn drop(&mut self) { + ElementInternals::drop(self) + } +} + impl AsElement for Window { fn as_element_rc(&self) -> Rc> { self.inner.clone() diff --git a/crates/craft_retained/src/layout/taffy_tree.rs b/crates/craft_retained/src/layout/taffy_tree.rs index 9f6d7192..5790e918 100644 --- a/crates/craft_retained/src/layout/taffy_tree.rs +++ b/crates/craft_retained/src/layout/taffy_tree.rs @@ -105,7 +105,7 @@ impl TaffyTree { self.is_layout_dirty = false; } - /// Remove the entire layout subtree. + /// Remove a specific `node` and its ancestors from the tree and drop it pub fn remove_subtree(&mut self, node: NodeId) { // Can we avoid this allocation? let children = self.inner.children(node).unwrap(); @@ -113,8 +113,23 @@ impl TaffyTree { for child in children { self.remove_subtree(child); } + self.remove_node(node); + self.request_layout(); + } + + /// Removes the `node`. + /// + /// The `node` is not removed from the tree entirely, it is simply no longer attached to its previous parent. + pub fn unparent_node(&mut self, node: NodeId) { + if let Some(parent) = self.inner.parent(node) { + self.inner.remove_child(parent, node).unwrap(); + self.request_layout(); + } + } - self.inner.remove(node).map(|_| ()).unwrap(); + /// Remove a specific node from the tree and drop it + pub fn remove_node(&mut self, node: NodeId) { + self.inner.remove(node).unwrap(); self.request_layout(); } @@ -158,7 +173,7 @@ impl TaffyTree { #[inline(always)] pub fn request_layout(&mut self) { self.is_layout_dirty = true; - self.is_apply_layout_dirty = Vec::new(); + self.is_apply_layout_dirty.clear(); } #[inline(always)] diff --git a/crates/craft_retained/src/lib.rs b/crates/craft_retained/src/lib.rs index 76b32bec..397ac743 100644 --- a/crates/craft_retained/src/lib.rs +++ b/crates/craft_retained/src/lib.rs @@ -12,7 +12,7 @@ pub use image; #[cfg(target_os = "android")] pub use winit::platform::android::activity::*; -pub use winit::window::{Cursor, CursorIcon, WindowAttributes}; +pub use winit::window::{Cursor, CursorIcon, Window as WinitWindow, WindowAttributes}; pub use crate::craftcallback::CraftCallback; pub use crate::options::CraftOptions; diff --git a/website/Cargo.toml b/website/Cargo.toml new file mode 100644 index 00000000..0d897a2d --- /dev/null +++ b/website/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "website" +version = "0.1.0" +edition = "2024" + +[dependencies] + +tracing-subscriber = "0.3.19" +tracing = "0.1.41" + +util = { path = "../examples/util" } + +serde = { version = "1.0.213", features = ["derive"] } +serde_json = "1.0.133" +web-sys = { version = "0.3.77", features = ["Window", "Location", "History"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies.craft_retained] +path = "../crates/craft_retained" +default-features = false +features = [ + "vello_hybrid_renderer", + "vello_hybrid_renderer", + "http_client", + "png", + "jpeg", + "accesskit", + "system_fonts", + "markdown" +] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.craft_retained] +path = "../crates/craft_retained" +default-features = false +features = [ + "vello_renderer", + "http_client", + "png", + "jpeg", + "accesskit", + "system_fonts", + "markdown" +] + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tracing-web = "0.1.3" +console_error_panic_hook = "0.1.7" + +[dependencies.reqwest] +workspace = true +default-features = false +features = ["rustls", "json"] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +open = "5" \ No newline at end of file diff --git a/website/assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg b/website/assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg new file mode 100644 index 00000000..d037aba6 Binary files /dev/null and b/website/assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg differ diff --git a/website/assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg b/website/assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg new file mode 100644 index 00000000..d982515b Binary files /dev/null and b/website/assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg differ diff --git a/website/assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg b/website/assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg new file mode 100644 index 00000000..57018bd9 Binary files /dev/null and b/website/assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg differ diff --git a/website/assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg b/website/assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg new file mode 100644 index 00000000..cb71bf7b Binary files /dev/null and b/website/assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg differ diff --git a/website/assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg b/website/assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg new file mode 100644 index 00000000..c5179378 Binary files /dev/null and b/website/assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg differ diff --git a/website/index.html b/website/index.html new file mode 100644 index 00000000..4bdd505f --- /dev/null +++ b/website/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/src/docs.rs b/website/src/docs.rs new file mode 100644 index 00000000..d23db73f --- /dev/null +++ b/website/src/docs.rs @@ -0,0 +1,19 @@ +use craft_retained::elements::{Container, Element}; +use craft_retained::pct; +use craft_retained::style::{Display, FlexDirection, Overflow, Unit}; + +use crate::router::NavigateFn; + +pub(crate) fn docs(_navigate_fn: NavigateFn) -> Container { + Container::new() + .width(pct(100)) + .overflow(Overflow::Visible, Overflow::Scroll) + .push( + Container::new() + .display(Display::Flex) + .width(pct(100)) + .margin(Unit::Px(0.0), Unit::Auto, Unit::Px(0.0), Unit::Auto) + .flex_direction(FlexDirection::Column) + .flex_grow(1.0), + ) +} diff --git a/website/src/index.rs b/website/src/index.rs new file mode 100644 index 00000000..bc74c7d3 --- /dev/null +++ b/website/src/index.rs @@ -0,0 +1,177 @@ +use craft_retained::elements::{Container, Element, Text}; +use craft_retained::style::{AlignItems, Display, FlexDirection, FlexWrap, FontWeight, JustifyContent, Overflow, Unit}; +use craft_retained::{Color, ResourceIdentifier, WinitWindow, palette, pct, px, rgb}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::link::Link; +use crate::router::{NavigateFn, Router}; +use crate::theme::{WRAPPER_PADDING_LEFT, WRAPPER_PADDING_RIGHT, wrapper}; +use crate::web_link::WebLink; + +fn hero_intro(navigate_fn: NavigateFn) -> Container { + let bg_wrapper = Container::new().width(pct(100)).background_color(rgb(45, 48, 53)); + + let inner_wrapper = wrapper() + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .padding( + Unit::Px(100.0), + WRAPPER_PADDING_RIGHT, + Unit::Px(100.0), + WRAPPER_PADDING_LEFT, + ); + + let inner_wrapper = inner_wrapper + .push( + Text::new("A Reactive GUI Framework for Rust") + .color(Color::WHITE) + /*.font_size(if window_ctx.inner_size().width <= MOBILE_MEDIA_QUERY_WIDTH { + 36.0 + } else { + 56.0 + })*/ + .font_size(56.0) + .line_height(1.0) + .max_width(px(680)) + .font_weight(FontWeight::BOLD) + .margin(px(0), px(0), px(32), px(0)), + ) + .push( + Text::new("Build your UI with regular Rust code.") + .line_height(1.0) + .color(Color::WHITE) + .font_size(20.0), + ); + + let github_button = Text::new("GitHub") + .display(Display::Flex) + .align_items(Some(AlignItems::Center)) + .justify_content(Some(JustifyContent::Center)) + .font_size(22.0) + .min_width(px(100)) + .border_width(px(1), px(1), px(1), px(1)) + .border_radius((8.0, 8.0), (8.0, 8.0), (8.0, 8.0), (8.0, 8.0)) + .padding(px(8), px(20), px(8), px(20)) + .border_color( + palette::css::WHITE, + palette::css::WHITE, + palette::css::WHITE, + palette::css::WHITE, + ) + .color(palette::css::WHITE); + + let craft_button = Text::new("Learn Craft") + .id("xxx") + .display(Display::Flex) + .align_items(Some(AlignItems::Center)) + .justify_content(Some(JustifyContent::Center)) + .font_size(22.0) + .min_width(px(100)) + .border_radius((8.0, 8.0), (8.0, 8.0), (8.0, 8.0), (8.0, 8.0)) + .padding(px(8), px(20), px(8), px(20)) + .background_color(rgb(69, 117, 230)) + .color(palette::css::WHITE); + + let buttons = Container::new() + .display(Display::Flex) + .wrap(FlexWrap::Wrap) + .gap(px(17), px(17)) + .margin(px(40), px(0), px(0), px(0)) + .push(Link("/docs", move || navigate_fn("/docs")).push(craft_button)) + .push(WebLink("https://github.com/craft-gui/craft").push(github_button)); + + let inner_wrapper = inner_wrapper.push(buttons); + + bg_wrapper.push(inner_wrapper) +} + +fn hero_features() -> Container { + fn hero_item(title: &str, text: &str, icon: ResourceIdentifier) -> Container { + let sub_title_color = Color::from_rgb8(70, 70, 70); + + let icon_title = Container::new() + // .push(TinyVg::new(icon)) + .push( + Text::new(title) + .font_weight(FontWeight::MEDIUM) + .font_size(24.0) + .margin(px(0), px(0), px(0), px(10)), + ); + + Container::new() + .gap(px(10), px(10)) + .flex_grow(1.0) + .flex_shrink(1.0) + .min_width(px(320)) + .flex_basis(pct(50)) + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .push(icon_title) + .push(Text::new(text).font_size(18.0).color(sub_title_color)) + } + + Container::new() + .background_color(rgb(247, 247, 247)) + .width(pct(100)) + .push( + wrapper() + .padding(Unit::Px(100.0), WRAPPER_PADDING_LEFT, Unit::Px(100.0), WRAPPER_PADDING_RIGHT) + .display(Display::Flex) + .wrap(FlexWrap::Wrap) + .gap(px(0), px(50)) + .push(Text::new("Features").width(pct(100)).font_size(36.0).font_weight(FontWeight::SEMIBOLD)) + .push( + hero_item( + "Reactive", + "When your data changes, we automatically re-run your view function.", + ResourceIdentifier::Bytes(include_bytes!("../assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Components", + "Components are reusable blocks that manage their own state and define both how they are rendered and how they respond to updates.", + ResourceIdentifier::Bytes(include_bytes!("../assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Pure Rust without macros", + "No macros.", + ResourceIdentifier::Bytes(include_bytes!("../assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Web-like styling", + "We use Taffy, an implementation of the CSS flexbox, block, and grid layout algorithms, for simple and familiar styling.", + ResourceIdentifier::Bytes(include_bytes!("../assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Cross Platform", + "Currently we support Windows, macOS, Linux, Web, and Android.", + ResourceIdentifier::Bytes(include_bytes!("../assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + + ) +} + +pub(crate) fn index_page(navigate_fn: NavigateFn) -> Container { + Container::new() + .width(pct(100)) + .overflow(Overflow::Visible, Overflow::Scroll) + .push( + Container::new() + .display(Display::Flex) + .width(pct(100)) + .margin(Unit::Px(0.0), Unit::Auto, Unit::Px(0.0), Unit::Auto) + .flex_direction(FlexDirection::Column) + .flex_grow(1.0) + .push(hero_intro(navigate_fn)) + .push(hero_features()), + ) +} diff --git a/website/src/link.rs b/website/src/link.rs new file mode 100644 index 00000000..46a27bf2 --- /dev/null +++ b/website/src/link.rs @@ -0,0 +1,18 @@ +use std::rc::Rc; + +use craft_retained::elements::{Container, Element}; +use craft_retained::events::ui_events::pointer::PointerButton; + +#[allow(non_snake_case)] +pub fn Link(href: &str, on_click: F) -> Container +where + F: Fn() + 'static, +{ + let on_click = Rc::new(on_click); + + Container::new().on_pointer_button_up(Rc::new(move |_event, pointer_button_event| { + if pointer_button_event.button == Some(PointerButton::Primary) { + on_click(); + } + })) +} diff --git a/website/src/main.rs b/website/src/main.rs new file mode 100644 index 00000000..204acafd --- /dev/null +++ b/website/src/main.rs @@ -0,0 +1,100 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use craft_retained::elements::{Container, Element, ElementInternals, Window}; +use craft_retained::{CraftOptions, craft_main, pct}; + +use crate::router::Router; + +mod docs; +mod index; +mod link; +mod navbar; +mod router; +mod theme; +mod web_link; + +pub(crate) struct WebsiteGlobalState { + /// The current route that we are viewing. + route: String, +} + +impl WebsiteGlobalState { + pub(crate) fn get_route(&self) -> String { + #[cfg(target_arch = "wasm32")] + let path: String; + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().expect("No window available."); + path = window + .location() + .pathname() + .map(|s| { + let trimmed_path = s.trim_end_matches('/'); + if trimmed_path.is_empty() { + "/".to_string() + } else { + trimmed_path.to_string() + } + }) + .unwrap_or("/".to_string()); + } + #[cfg(not(target_arch = "wasm32"))] + let path = self.route.clone(); + path + } + + pub(crate) fn set_route(&mut self, route: &str) { + self.route = route.to_string(); + + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().unwrap(); + let history = window.history().unwrap(); + + history + .push_state_with_url(&web_sys::wasm_bindgen::JsValue::NULL, "", Some(route)) + .unwrap(); + } + } + + pub fn load_route(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + { + // NOTE: In Git Bash, use `cargo run -- //examples`. + let route = std::env::args().nth(1).unwrap_or_else(|| "/".to_string()); + self.set_route(route.as_str()); + } + } +} + +impl Default for WebsiteGlobalState { + fn default() -> Self { + WebsiteGlobalState { + route: "/".to_string(), + } + } +} + +fn main() { + let options = CraftOptions { + ..Default::default() + }; + + #[allow(unused_mut)] + let mut global_state = Rc::new(RefCell::new(WebsiteGlobalState::default())); + + util::setup_logging(); + + global_state.borrow_mut().load_route(); + + let page_wrapper = Router::new(global_state.clone()); + + /* let root = page_wrapper.borrow().root.clone(); + + root.inner.borrow().print_tree_ids(4); + */ + page_wrapper.borrow().navigate(); + + craft_main(options); +} diff --git a/website/src/navbar.rs b/website/src/navbar.rs new file mode 100644 index 00000000..6ce616e5 --- /dev/null +++ b/website/src/navbar.rs @@ -0,0 +1,69 @@ +use crate::link::Link; +use crate::router::NavigateFn; +use crate::theme::{NAVBAR_BACKGROUND_COLOR, NAVBAR_TEXT_COLOR, wrapper}; +use craft_retained::elements::{Container, Element, Text}; +use craft_retained::style::{AlignItems, Display, FontWeight, JustifyContent, Unit}; +use craft_retained::{pct, px, rgb}; + +pub const NAVBAR_HEIGHT: f32 = 60.0; + +fn create_link(navigate_fn: NavigateFn, label: &str, route: &str) -> Container { + let route_owned = route.to_string(); + let nav = navigate_fn.clone(); + Link(route, move || { + nav(&route_owned); + }) + .push( + Text::new(label) + .id(format!("route_{route}").as_str()) + .margin(px(0), px(12), px(0), px(0)) + .font_size(16.0) + .selectable(false) + .color(NAVBAR_TEXT_COLOR), + ) + /*.hovered() + .color(NAVBAR_TEXT_HOVERED_COLOR) + .underline(1.0, Color::BLACK, None) + .margin(px(0), "12px", px(0), px(0)) + .font_size(16.5) + .disable_selection() + .normal()*/ +} + +pub fn navbar(navigate_fn: NavigateFn) -> Container { + let border_color = rgb(240, 240, 240); + let container = Container::new() + .width(pct(100)) + .height(Unit::Px(NAVBAR_HEIGHT)) + .min_height(Unit::Px(NAVBAR_HEIGHT)) + .max_height(Unit::Px(NAVBAR_HEIGHT)) + .border_width(px(0), px(0), px(2), px(0)) + .border_color(border_color, border_color, border_color, border_color) + .background_color(NAVBAR_BACKGROUND_COLOR); + + let wrapper = wrapper() + .display(Display::Flex) + .justify_content(Some(JustifyContent::SpaceBetween)) + .align_items(Some(AlignItems::Center)) + // Left + .push( + Container::new() + .display(Display::Flex) + .justify_content(Some(JustifyContent::Center)) + .align_items(Some(AlignItems::Center)) + .push( + create_link(navigate_fn.clone(), "Craft", "/") + .font_size(32.0) + .font_weight(FontWeight::BOLD) + .margin(px(0), px(24), px(0), px(0)), /*.hovered() + .font_size(32.0) + .font_weight(Weight::BOLD) + .margin(px(0), "24px", px(0), px(0)),*/ + ) + .push(create_link(navigate_fn.clone(), "Home", "/").margin(px(0), px(12), px(0), px(0))) + .push(create_link(navigate_fn.clone(), "Docs", "/docs").margin(px(0), px(12), px(0), px(0))) + .push(create_link(navigate_fn.clone(), "Examples", "/examples").margin(px(0), px(12), px(0), px(0))), + ); + + container.push(wrapper) +} diff --git a/website/src/router.rs b/website/src/router.rs new file mode 100644 index 00000000..f0f3dc44 --- /dev/null +++ b/website/src/router.rs @@ -0,0 +1,76 @@ +use craft_retained::elements::{Container, Element, Window}; +use craft_retained::pct; +use craft_retained::style::{Display, FlexDirection}; +use std::cell::RefCell; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +use crate::docs::docs; +use crate::index::index_page; +use crate::navbar::navbar; +use crate::theme::BODY_BACKGROUND_COLOR; +use crate::{WebsiteGlobalState, docs, index}; + +#[derive(Clone)] +pub struct Router { + pub root: Window, + global_state: Rc>, + index: Container, + docs: Container, +} + +pub type NavigateFn = Rc; + +impl Router { + pub fn new(global_state: Rc>) -> Rc> { + let state = global_state.clone(); + Rc::new_cyclic(|me: &Weak>| { + let me = me.clone(); + + let navigate_logic: NavigateFn = Rc::new(move |route| { + state.borrow_mut().set_route(route); + if let Some(router) = me.upgrade() { + router.borrow().navigate(); + } + }); + + let window = Window::new("Craft Gui") + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .width(pct(100)) + .height(pct(100)) + .push(navbar(navigate_logic.clone())) + .background_color(BODY_BACKGROUND_COLOR); + + RefCell::new(Self { + root: window.clone(), + index: index_page(navigate_logic.clone()), + docs: docs(navigate_logic.clone()), + global_state: global_state.clone(), + }) + }) + } + + fn set_content(&self, container: Container) { + if let Some(current_content) = self.root.get_children().get(1) { + self.root + .remove_child(current_content.clone()) + .expect("Failed to remove child"); + } + self.root.clone().push(container); + } + + pub fn navigate(&self) { + let page = match self.global_state.borrow().route.as_str() { + "/" => self.index.clone(), + "/docs" => self.docs.clone(), + _ => self.index.clone(), + }; + + self.set_content(page); + } + + /*pub fn window(&self) -> Arc { + self.root.inner.borrow().winit_window().expect("No widow") + }*/ +} diff --git a/website/src/theme.rs b/website/src/theme.rs new file mode 100644 index 00000000..17456eb1 --- /dev/null +++ b/website/src/theme.rs @@ -0,0 +1,33 @@ +use craft_retained::elements::{Container, Element}; +use craft_retained::style::Unit; +use craft_retained::{Color, pct}; + +pub(crate) const BODY_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); + +pub(crate) const NAVBAR_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); +pub(crate) const NAVBAR_TEXT_COLOR: Color = Color::from_rgb8(50, 50, 50); +//pub(crate) const NAVBAR_TEXT_HOVERED_COLOR: Color = Color::from_rgb8(0, 0, 0); + +//pub(crate) const ACTIVE_LINK_COLOR: Color = Color::from_rgb8(42, 108, 200); +//pub(crate) const DEFAULT_LINK_COLOR: Color = Color::from_rgb8(102, 102, 102); + +pub(crate) const WRAPPER_MAX_WIDTH: Unit = Unit::Px(1300.0); +pub(crate) const WRAPPER_MARGIN_LEFT: Unit = Unit::Auto; +pub(crate) const WRAPPER_MARGIN_RIGHT: Unit = Unit::Auto; +pub(crate) const WRAPPER_PADDING_LEFT: Unit = Unit::Px(20.0); +pub(crate) const WRAPPER_PADDING_RIGHT: Unit = Unit::Px(20.0); + +//pub(crate) const MOBILE_MEDIA_QUERY_WIDTH: u32 = 850; + +pub(crate) fn wrapper() -> Container { + Container::new() + .margin(Unit::Px(0.0), WRAPPER_MARGIN_RIGHT, Unit::Px(0.0), WRAPPER_MARGIN_LEFT) + .padding( + Unit::Px(0.0), + WRAPPER_PADDING_RIGHT, + Unit::Px(0.0), + WRAPPER_PADDING_LEFT, + ) + .width(pct(100)) + .max_width(WRAPPER_MAX_WIDTH) +} diff --git a/website/src/web_link.rs b/website/src/web_link.rs new file mode 100644 index 00000000..564fbdc6 --- /dev/null +++ b/website/src/web_link.rs @@ -0,0 +1,26 @@ +use std::rc::Rc; + +use craft_retained::elements::{Container, Element}; +use craft_retained::events::ui_events::pointer::PointerButton; + +#[allow(non_snake_case)] +pub fn WebLink(href: &str) -> Container { + let href = href.to_string(); + + Container::new().on_pointer_button_up(Rc::new(move |_event, pointer_button_event| { + if pointer_button_event.button == Some(PointerButton::Primary) { + #[cfg(target_arch = "wasm32")] + { + if let Some(win) = web_sys::window() { + // Use the captured owned string + let _ = win.open_with_url(&href); + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + open::that(&href).unwrap(); + } + } + })) +} diff --git a/website/wasm-build.sh b/website/wasm-build.sh new file mode 100644 index 00000000..07fb3318 --- /dev/null +++ b/website/wasm-build.sh @@ -0,0 +1,12 @@ +set -e + + +#rustup target add wasm32-unknown-unknown +#cargo install -f wasm-bindgen-cli +#cargo install simple-http-server + +cargo build --target wasm32-unknown-unknown --release + +wasm-bindgen ../target/wasm32-unknown-unknown/release/website.wasm --target web --no-typescript --out-dir dist --out-name website +cp index.html dist/index.html +simple-http-server dist -c wasm,html,js --try-file dist/index.html -i --coep --coop --ip 0.0.0.0 \ No newline at end of file