From 801e88f4c57395b9778c9f549d00ebf3a16d4402 Mon Sep 17 00:00:00 2001 From: Shamil Date: Mon, 30 Oct 2023 13:32:16 +0300 Subject: [PATCH] feat: remove Yew (#60) --- Cargo.toml | 35 +- dist/index.html | 37 -- dist/ng_blur/index.html | 15 - dist/ng_buttons/index.html | 15 - dist/ng_grid/index.html | 15 - dist/ng_hello_world/index.html | 15 - dist/ng_pointer/index.html | 15 - dist/ng_rows_cols/index.html | 15 - dist/ng_svg/index.html | 15 - dist/ng_text/index.html | 15 - examples/blur.rs | 26 +- examples/buttons.rs | 43 +- examples/grid.rs | 66 +-- examples/hello_world.rs | 21 +- examples/logo-ng.svg-rs | 1 - examples/logo.svg-rs | 2 +- examples/ng_blur.rs | 21 - examples/ng_buttons.rs | 35 -- examples/ng_grid.rs | 53 --- examples/ng_hello_world.rs | 21 - examples/ng_pointer.rs | 21 - examples/ng_rows_cols.rs | 27 -- examples/ng_svg.rs | 30 -- examples/ng_text.rs | 49 --- examples/pointer.rs | 26 +- examples/rows_cols.rs | 36 +- examples/svg.rs | 40 +- examples/text.rs | 70 ++-- scripts/examples.sh | 8 - src/{ng => }/frame.rs | 0 src/lib.rs | 50 +-- src/ng.rs | 75 ---- src/ng/align.rs | 27 -- src/ng/button.rs | 87 ---- src/ng/color.rs | 321 --------------- src/ng/column.rs | 95 ----- src/ng/grid.rs | 65 --- src/ng/label.rs | 44 -- src/ng/row.rs | 95 ----- src/ng/slide.rs | 141 ------- src/ng/slideshow.rs | 180 --------- src/ng/svg.rs | 104 ----- src/ng/text.rs | 271 ------------- src/{properties/mod.rs => properties.rs} | 0 src/properties/color.rs | 11 + src/utils.rs | 51 --- src/widgets.rs | 29 +- src/widgets/button.rs | 157 +++----- src/widgets/column.rs | 132 +++--- src/widgets/grid.rs | 101 ++--- src/widgets/label.rs | 74 ++-- src/widgets/row.rs | 132 +++--- src/widgets/slide.rs | 313 ++++++-------- src/widgets/slideshow.rs | 341 +++++++--------- src/widgets/svg.rs | 95 +++-- src/widgets/text.rs | 492 +++++++++++------------ 56 files changed, 1086 insertions(+), 3185 deletions(-) delete mode 100644 dist/ng_blur/index.html delete mode 100644 dist/ng_buttons/index.html delete mode 100644 dist/ng_grid/index.html delete mode 100644 dist/ng_hello_world/index.html delete mode 100644 dist/ng_pointer/index.html delete mode 100644 dist/ng_rows_cols/index.html delete mode 100644 dist/ng_svg/index.html delete mode 100644 dist/ng_text/index.html delete mode 100644 examples/logo-ng.svg-rs delete mode 100644 examples/ng_blur.rs delete mode 100644 examples/ng_buttons.rs delete mode 100644 examples/ng_grid.rs delete mode 100644 examples/ng_hello_world.rs delete mode 100644 examples/ng_pointer.rs delete mode 100644 examples/ng_rows_cols.rs delete mode 100644 examples/ng_svg.rs delete mode 100644 examples/ng_text.rs rename src/{ng => }/frame.rs (100%) delete mode 100644 src/ng.rs delete mode 100644 src/ng/align.rs delete mode 100644 src/ng/button.rs delete mode 100644 src/ng/color.rs delete mode 100644 src/ng/column.rs delete mode 100644 src/ng/grid.rs delete mode 100644 src/ng/label.rs delete mode 100644 src/ng/row.rs delete mode 100644 src/ng/slide.rs delete mode 100644 src/ng/slideshow.rs delete mode 100644 src/ng/svg.rs delete mode 100644 src/ng/text.rs rename src/{properties/mod.rs => properties.rs} (100%) delete mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 7c490ba..42f2c10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.2" edition = "2021" license = "MIT" description = "Lerni content framework" -repository = "https://github.com/lerni-edu/lerni" +repository = "https://github.com/noviga/lerni" include = ["src/**/*.rs", "examples/**/*.rs"] [dependencies] @@ -15,7 +15,6 @@ gloo-utils = "0.2.0" leptos = { version = "0.5.1", features = ["csr"] } leptos-use = "0.8.0" rand = { version = "0.8.5", default-features = false, features = ["getrandom"] } -yew = { version = "0.21.0", features = ["csr"] } wasm-bindgen = "0.2.84" [dependencies.web-sys] @@ -43,38 +42,6 @@ crate-type = ["cdylib"] name = "hello_world" crate-type = ["cdylib"] -[[example]] -name = "ng_blur" -crate-type = ["cdylib"] - -[[example]] -name = "ng_buttons" -crate-type = ["cdylib"] - -[[example]] -name = "ng_grid" -crate-type = ["cdylib"] - -[[example]] -name = "ng_hello_world" -crate-type = ["cdylib"] - -[[example]] -name = "ng_pointer" -crate-type = ["cdylib"] - -[[example]] -name = "ng_rows_cols" -crate-type = ["cdylib"] - -[[example]] -name = "ng_svg" -crate-type = ["cdylib"] - -[[example]] -name = "ng_text" -crate-type = ["cdylib"] - [[example]] name = "pointer" crate-type = ["cdylib"] diff --git a/dist/index.html b/dist/index.html index 6eb2cfb..48cccea 100644 --- a/dist/index.html +++ b/dist/index.html @@ -43,43 +43,6 @@

Lerni Examples

(source) - -

New Generation (Leptos-based) Lerni Examples

-

⚠️ Work in progress!

- diff --git a/dist/ng_blur/index.html b/dist/ng_blur/index.html deleted file mode 100644 index 6638b30..0000000 --- a/dist/ng_blur/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Blur" Example - - - - - - - - - diff --git a/dist/ng_buttons/index.html b/dist/ng_buttons/index.html deleted file mode 100644 index 0ec74bf..0000000 --- a/dist/ng_buttons/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Buttons" Example - - - - - - - - - diff --git a/dist/ng_grid/index.html b/dist/ng_grid/index.html deleted file mode 100644 index 3372b5b..0000000 --- a/dist/ng_grid/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Grid" Example - - - - - - - - - diff --git a/dist/ng_hello_world/index.html b/dist/ng_hello_world/index.html deleted file mode 100644 index efd5690..0000000 --- a/dist/ng_hello_world/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Hello, World!" Example - - - - - - - - - diff --git a/dist/ng_pointer/index.html b/dist/ng_pointer/index.html deleted file mode 100644 index b7aaac4..0000000 --- a/dist/ng_pointer/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Pointer" Example - - - - - - - - - diff --git a/dist/ng_rows_cols/index.html b/dist/ng_rows_cols/index.html deleted file mode 100644 index de435be..0000000 --- a/dist/ng_rows_cols/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Rows & Columns" Example - - - - - - - - - diff --git a/dist/ng_svg/index.html b/dist/ng_svg/index.html deleted file mode 100644 index d589492..0000000 --- a/dist/ng_svg/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "SVG" Example - - - - - - - - - diff --git a/dist/ng_text/index.html b/dist/ng_text/index.html deleted file mode 100644 index 62c6dd3..0000000 --- a/dist/ng_text/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Lerni "Text" Example - - - - - - - - - diff --git a/examples/blur.rs b/examples/blur.rs index de39698..e758902 100644 --- a/examples/blur.rs +++ b/examples/blur.rs @@ -1,23 +1,21 @@ -use lerni::{properties::Color, widgets::*}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn Blur() -> Html { - let blur = use_state(|| false); - let onclick = { - let blur = blur.clone(); - Callback::from(move |_| blur.set(!*blur)) - }; +#[component] +pub fn Blur() -> impl IntoView { + let (blur, set_blur) = create_signal(false); + let on_click = move |_| set_blur.set(!blur.get()); - html! { - - } } #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(Blur); } diff --git a/examples/buttons.rs b/examples/buttons.rs index 1c1aabd..73722cd 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -1,28 +1,29 @@ -use lerni::{properties::*, widgets::*}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn Buttons() -> Html { - let counter = use_state(|| 0); - let onclick = { - let counter = counter.clone(); - Callback::from(move |_| counter.set(*counter + 1)) +#[component] +pub fn Buttons() -> impl IntoView { + let (counter, set_counter) = create_signal(0); + let on_click = move |_| { + logging::log!("Clicked"); + set_counter.set(counter.get() + 1); }; - html! { + view! { - + + + + + + + + } @@ -30,5 +31,5 @@ pub fn Buttons() -> Html { #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(Buttons); } diff --git a/examples/grid.rs b/examples/grid.rs index 40f61da..bb77b73 100644 --- a/examples/grid.rs +++ b/examples/grid.rs @@ -1,32 +1,46 @@ -use lerni::{ - properties::{Align, Color, VAlign}, - widgets::*, -}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn GridExample() -> Html { - html! { +#[component] +pub fn GridExample() -> impl IntoView { + view! { - @@ -35,5 +49,5 @@ pub fn GridExample() -> Html { #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(GridExample); } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 0f8ce90..e70de82 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,18 +1,21 @@ -use lerni::{properties::Color, widgets::*}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn HelloWorld() -> Html { - html! { +#[component] +pub fn HelloWorld() -> impl IntoView { + view! { - - + + + + + + } } #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(HelloWorld); } diff --git a/examples/logo-ng.svg-rs b/examples/logo-ng.svg-rs deleted file mode 100644 index 25b3dcb..0000000 --- a/examples/logo-ng.svg-rs +++ /dev/null @@ -1 +0,0 @@ -view! { } diff --git a/examples/logo.svg-rs b/examples/logo.svg-rs index c522df5..25b3dcb 100644 --- a/examples/logo.svg-rs +++ b/examples/logo.svg-rs @@ -1 +1 @@ -html!() +view! { } diff --git a/examples/ng_blur.rs b/examples/ng_blur.rs deleted file mode 100644 index 9de394b..0000000 --- a/examples/ng_blur.rs +++ /dev/null @@ -1,21 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn Blur() -> impl IntoView { - let (blur, set_blur) = create_signal(false); - let on_click = move |_| set_blur.set(!blur.get()); - - view! { - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(Blur); -} diff --git a/examples/ng_buttons.rs b/examples/ng_buttons.rs deleted file mode 100644 index 66282bc..0000000 --- a/examples/ng_buttons.rs +++ /dev/null @@ -1,35 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn Buttons() -> impl IntoView { - let (counter, set_counter) = create_signal(0); - let on_click = move |_| { - logging::log!("Clicked"); - set_counter.set(counter.get() + 1); - }; - - view! { - - - - - - - - - - - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(Buttons); -} diff --git a/examples/ng_grid.rs b/examples/ng_grid.rs deleted file mode 100644 index 00f638f..0000000 --- a/examples/ng_grid.rs +++ /dev/null @@ -1,53 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn GridExample() -> impl IntoView { - view! { - - - - - - - - - - - - - - - - - - - - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(GridExample); -} diff --git a/examples/ng_hello_world.rs b/examples/ng_hello_world.rs deleted file mode 100644 index c0e83d1..0000000 --- a/examples/ng_hello_world.rs +++ /dev/null @@ -1,21 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn HelloWorld() -> impl IntoView { - view! { - - - - - - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(HelloWorld); -} diff --git a/examples/ng_pointer.rs b/examples/ng_pointer.rs deleted file mode 100644 index 686cff4..0000000 --- a/examples/ng_pointer.rs +++ /dev/null @@ -1,21 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn Pointer() -> impl IntoView { - let (pointer, set_pointer) = create_signal(true); - let on_click = move |_| set_pointer.set(!pointer.get()); - - view! { - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(Pointer); -} diff --git a/examples/ng_rows_cols.rs b/examples/ng_rows_cols.rs deleted file mode 100644 index a15ef61..0000000 --- a/examples/ng_rows_cols.rs +++ /dev/null @@ -1,27 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn RowsCols() -> impl IntoView { - view! { - - - - - - - - - - - - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(RowsCols); -} diff --git a/examples/ng_svg.rs b/examples/ng_svg.rs deleted file mode 100644 index d689713..0000000 --- a/examples/ng_svg.rs +++ /dev/null @@ -1,30 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn SvgExample() -> impl IntoView { - view! { - - - - {include!("logo-ng.svg-rs")} - - {include!("logo-ng.svg-rs")} - - - {include!("logo-ng.svg-rs")} - {include!("logo-ng.svg-rs")} - { include!("logo-ng.svg-rs")} - {include!("logo-ng.svg-rs")} - - {include!("logo-ng.svg-rs")} - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(SvgExample); -} diff --git a/examples/ng_text.rs b/examples/ng_text.rs deleted file mode 100644 index e0da567..0000000 --- a/examples/ng_text.rs +++ /dev/null @@ -1,49 +0,0 @@ -use leptos::*; -use lerni::ng::*; - -#[component] -pub fn TextExample() -> impl IntoView { - let words_read1 = create_rw_signal(0); - let letters_read1 = create_rw_signal(0); - let letters_total1 = create_rw_signal(0); - - let words_read2 = create_rw_signal(0); - let letters_read2 = create_rw_signal(0); - let letters_total2 = create_rw_signal(0); - - let words_read3 = create_rw_signal(0); - let letters_read3 = create_rw_signal(0); - let letters_total3 = create_rw_signal(0); - - view! { - - - - - { "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } - { "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." } - - - - - - - { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." } - { "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } - - - - { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." } - { "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } - - - - - - } -} - -#[wasm_bindgen(start)] -pub fn main() { - lerni::ng::start(TextExample); -} diff --git a/examples/pointer.rs b/examples/pointer.rs index c2e22c1..b88052e 100644 --- a/examples/pointer.rs +++ b/examples/pointer.rs @@ -1,23 +1,21 @@ -use lerni::widgets::*; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn Pointer() -> Html { - let pointer = use_state(|| true); - let onclick = { - let pointer = pointer.clone(); - Callback::from(move |_| pointer.set(!*pointer)) - }; +#[component] +pub fn Pointer() -> impl IntoView { + let (pointer, set_pointer) = create_signal(true); + let on_click = move |_| set_pointer.set(!pointer.get()); - html! { - - } } #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(Pointer); } diff --git a/examples/rows_cols.rs b/examples/rows_cols.rs index 173f5b3..d5f0c0a 100644 --- a/examples/rows_cols.rs +++ b/examples/rows_cols.rs @@ -1,25 +1,21 @@ -use lerni::{ - properties::{Align, VAlign}, - widgets::*, -}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn RowsCols() -> Html { - html! { +#[component] +pub fn RowsCols() -> impl IntoView { + view! { - - } @@ -27,5 +23,5 @@ pub fn RowsCols() -> Html { #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(RowsCols); } diff --git a/examples/svg.rs b/examples/svg.rs index 08b0d20..058f438 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,24 +1,24 @@ -use lerni::{ - properties::{Align, VAlign}, - widgets::*, -}; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn SvgExample() -> Html { - html! { - +#[component] +pub fn SvgExample() -> impl IntoView { + view! { + - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } - { include!("logo.svg-rs") } + + {include!("logo.svg-rs")} + + {include!("logo.svg-rs")} + + + {include!("logo.svg-rs")} + {include!("logo.svg-rs")} + { include!("logo.svg-rs")} + {include!("logo.svg-rs")} + + {include!("logo.svg-rs")} + } @@ -26,5 +26,5 @@ pub fn SvgExample() -> Html { #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(SvgExample); } diff --git a/examples/text.rs b/examples/text.rs index 33127d0..027453d 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -1,64 +1,42 @@ -use lerni::widgets::*; -use wasm_bindgen::prelude::wasm_bindgen; -use yew::prelude::*; +use leptos::*; +use lerni::*; -#[function_component] -pub fn TextExample() -> Html { - let words_read1 = use_state(|| 0); - let read1 = use_state(|| "".to_string()); - let onread1 = { - let words_read1 = words_read1.clone(); - let read1 = read1.clone(); - Callback::from(move |(words_read, letters, total)| { - words_read1.set(words_read); - read1.set(format!("{letters} / {total}")); - }) - }; +#[component] +pub fn TextExample() -> impl IntoView { + let words_read1 = create_rw_signal(0); + let letters_read1 = create_rw_signal(0); + let letters_total1 = create_rw_signal(0); - let words_read2 = use_state(|| 0); - let read2 = use_state(|| "".to_string()); - let onread2 = { - let words_read2 = words_read2.clone(); - let read2 = read2.clone(); - Callback::from(move |(words_read, letters, total)| { - words_read2.set(words_read); - read2.set(format!("{letters} / {total}")); - }) - }; + let words_read2 = create_rw_signal(0); + let letters_read2 = create_rw_signal(0); + let letters_total2 = create_rw_signal(0); - let words_read3 = use_state(|| 0); - let read3 = use_state(|| "".to_string()); - let onread3 = { - let words_read3 = words_read3.clone(); - let read3 = read3.clone(); - Callback::from(move |(words_read, letters, total)| { - words_read3.set(words_read); - read3.set(format!("{letters} / {total}")); - }) - }; + let words_read3 = create_rw_signal(0); + let letters_read3 = create_rw_signal(0); + let letters_total3 = create_rw_signal(0); - html! { + view! { - - - + + + { "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } { "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." } - - - + + { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." } { "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } - @@ -67,5 +45,5 @@ pub fn TextExample() -> Html { #[wasm_bindgen(start)] pub fn main() { - lerni::start::(); + lerni::start(TextExample); } diff --git a/scripts/examples.sh b/scripts/examples.sh index e63ccbb..3eaab59 100755 --- a/scripts/examples.sh +++ b/scripts/examples.sh @@ -13,14 +13,6 @@ blur \ buttons \ grid \ hello_world \ -ng_blur \ -ng_buttons \ -ng_grid \ -ng_hello_world \ -ng_pointer \ -ng_rows_cols \ -ng_svg \ -ng_text \ pointer \ rows_cols \ svg \ diff --git a/src/ng/frame.rs b/src/frame.rs similarity index 100% rename from src/ng/frame.rs rename to src/frame.rs diff --git a/src/lib.rs b/src/lib.rs index 256761a..6453ee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,50 +2,44 @@ #![warn(missing_docs)] -pub mod ng; -pub mod properties; -pub mod utils; -pub mod widgets; +mod frame; +mod properties; +mod widgets; -use yew::{BaseComponent, Renderer}; +pub use frame::*; +pub use properties::*; +pub use widgets::*; + +pub use wasm_bindgen::prelude::wasm_bindgen; + +use leptos::*; /// Start function. /// /// # Example /// /// ```no_run -/// use yew::prelude::*; -/// use wasm_bindgen::prelude::wasm_bindgen; +/// use leptos::*; +/// use lerni::*; /// -/// #[function_component] -/// pub fn HelloWorld() -> Html { -/// html!("Hello, world!") +/// #[component] +/// pub fn HelloWorld() -> impl IntoView { +/// view! { +/// "Hello, world!" +/// } /// } /// /// #[wasm_bindgen(start)] /// pub fn main() { -/// lerni::start::(); +/// lerni::start(HelloWorld); /// } /// ``` -pub fn start() +pub fn start(f: F) where - ::Properties: Default, + F: Fn() -> N + 'static, + N: IntoView, { - Renderer::::new().render(); -} - -/// Debug macro. -#[macro_export] -macro_rules! debug { - ($arg:literal) => { - web_sys::console::log_1(&format!("{}", $arg).into()) - }; - ($arg:expr) => { - web_sys::console::log_1(&format!("{:?}", $arg).into()) - }; - ($fmt:literal $(, $args:expr)+) => { - web_sys::console::log_1(&format!($fmt $(, $args)+).into()) - }; + leptos::mount_to_body(f); } /// Keyboard key codes. diff --git a/src/ng.rs b/src/ng.rs deleted file mode 100644 index 9114e49..0000000 --- a/src/ng.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! New generation Lerni based on Leptos. - -pub use wasm_bindgen::prelude::wasm_bindgen; - -mod align; -pub use align::{Align, VAlign}; - -mod button; -pub use button::Button; - -mod color; -pub use color::Color; - -mod column; -pub use column::Column; - -mod frame; -pub use frame::*; - -mod grid; -pub use grid::Grid; - -mod label; -pub use label::Label; - -mod row; -pub use row::Row; - -mod slide; -pub use slide::Slide; - -mod slideshow; -pub use slideshow::SlideShow; - -mod svg; -pub use svg::{Svg, SvgFile}; - -mod text; -pub use text::Text; - -use leptos::*; - -/// Additional information provided to all slides. -#[derive(Clone, Copy, Default, Debug, PartialEq)] -pub struct Metadata { - /// Visibility flag. - pub visible: bool, - /// Teacher mode flag. - pub teacher_mode: bool, - /// Pointer on/off flag. - pub pointer: bool, -} - -/// Calculates the width of the slide. -pub fn calc_width(margin: i32) -> i32 { - let elem = web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.document_element()); - if let Some(elem) = elem { - let width = elem.client_width(); - let height = elem.client_height(); - width.min((height - margin) * 16 / 9) - } else { - 0 - } -} - -/// The main entry point. -pub fn start(f: F) -where - F: Fn() -> N + 'static, - N: IntoView, -{ - leptos::mount_to_body(f); -} diff --git a/src/ng/align.rs b/src/ng/align.rs deleted file mode 100644 index b1cc981..0000000 --- a/src/ng/align.rs +++ /dev/null @@ -1,27 +0,0 @@ -/// Horizontal align. -#[derive(Clone, Default, PartialEq)] -pub enum Align { - /// Left horizontal align. - Left, - /// Center horizontal align. - #[default] - Center, - /// Right horizontal align. - Right, - /// Fill all horizontal space. - Fill, -} - -/// Vertical align. -#[derive(Clone, Default, PartialEq)] -pub enum VAlign { - /// Top vertical align. - Top, - /// Middle vertical align. - #[default] - Middle, - /// Bottom vertical align. - Bottom, - /// Fill all vertical space. - Fill, -} diff --git a/src/ng/button.rs b/src/ng/button.rs deleted file mode 100644 index e9024d4..0000000 --- a/src/ng/button.rs +++ /dev/null @@ -1,87 +0,0 @@ -use leptos::*; -use web_sys::MouseEvent; - -use crate::ng::{provide_frame, use_frame, Align, Color, Frame, Label, VAlign}; - -const WIDTH: i32 = 400; -const HEIGHT: i32 = 150; - -#[component] -pub fn Button( - on_click: CB, - #[prop(optional)] text_bold: bool, - #[prop(default = WIDTH)] width: i32, - #[prop(default = HEIGHT)] height: i32, - #[prop(default = 24)] radius: i32, - #[prop(optional)] font: String, - #[prop(default = 48)] font_size: i32, - #[prop(default = Color::AliceBlue)] color: Color, - #[prop(default = Color::Black)] text_color: Color, - #[prop(default = 12)] border_width: i32, - #[prop(default = Color::RoyalBlue)] border_color: Color, - #[prop(default = Align::Center)] align: Align, - #[prop(default = VAlign::Middle)] valign: VAlign, - children: Children, -) -> impl IntoView -where - CB: FnMut(MouseEvent) + 'static, -{ - let f = use_frame(); - - let width = if align == Align::Fill { f.width } else { width }; - let height = if valign == VAlign::Fill { - f.height - } else { - height - }; - - let x = match align { - Align::Left | Align::Fill => f.x, - Align::Center => f.x + (f.width - width) / 2, - Align::Right => f.x + f.width - width, - }; - let y = match valign { - VAlign::Top | VAlign::Fill => f.y, - VAlign::Middle => f.y + (f.height - height) / 2, - VAlign::Bottom => f.y + f.height - height, - }; - - let frame = Frame { - x, - y, - width, - height, - }; - provide_frame(frame); - - let x = x + border_width / 2; - let y = y + border_width / 2; - let width = width - border_width; - let height = height - border_width; - - let (border, set_border) = create_signal(border_width); - let on_mousedown = move |_| set_border.set(border_width + 6); - let on_mouseup = move |_| set_border.set(border_width); - - view! { - - - } -} diff --git a/src/ng/color.rs b/src/ng/color.rs deleted file mode 100644 index dade2e9..0000000 --- a/src/ng/color.rs +++ /dev/null @@ -1,321 +0,0 @@ -use derive_more::Display; -use leptos::{Attribute, IntoAttribute}; - -// TODO: Define all colors - -/// Helper type to work with colors. -#[derive(Clone, Copy, Default, Display, PartialEq)] -pub enum Color { - /// No color. - #[default] - None, - /// Arbitrary color with the string value. - #[display(fmt = "{_0}")] - Value(&'static str), - - /// Alice Blue color. - AliceBlue, - /// Antique White color. - AntiqueWhite, - /// - Aqua, - /// - Aquamarine, - /// - Azure, - /// - Aeige, - /// - Bisque, - /// - Black, - /// - BlanchedAlmond, - /// - Blue, - /// - BlueViolet, - /// - Brown, - /// - BurlyWood, - /// - CadetBlue, - /// - Chartreuse, - /// - Chocolate, - /// - Coral, - /// - CornflowerBlue, - /// - Cornsilk, - /// - Crimson, - /// - Cyan, - /// - DarkBlue, - /// - DarkCyan, - /// - DarkGoldenrod, - /// - DarkGray, - /// - DarkGreen, - /// - DarkGrey, - /// - DarkKhaki, - /// - DarkMagenta, - /// - DarkOliveGreen, - /// - DarkOrange, - /// - DarkOrchid, - /// - DarkRed, - /// - DarkSalmon, - /// - DarkSeaGreen, - /// - DarkSlateBlue, - /// - DarkSlateGray, - /// - DarkSlateGrey, - /// - /// - DarkTurquoise, - /// - DarkViolet, - /// - DeepPink, - /// - DeepSkyBlue, - /// - DimGray, - /// - DimGrey, - /// - DodgerBlue, - /// - Firebrick, - /// - FloralWhite, - /// - ForestGreen, - /// - Fuchsia, - /// - Gainsboro, - /// - GhostWhite, - /// - Gold, - /// - Goldenrod, - /// - Gray, - /// - Green, - /// - GreenYellow, - /// - Grey, - /// - Honeydew, - /// - HotPink, - /// - IndianRed, - /// - Indigo, - /// - Ivory, - /// - Khaki, - /// - Lavender, - /// - LavenderBlush, - /// - LawnGreen, - /// - Lemonchiffon, - /// - LightBlue, - /// - LightCoral, - /// - LightCyan, - /// - LightGoldenrodYellow, - /// - LightGray, - /// - LightGreen, - /// - LightGrey, - /// - LightPink, - /// - LightSalmon, - /// - LightSeaGreen, - /// - LightSkyBlue, - /// - LightSlateGray, - /// - LightSlateGrey, - /// - LightsteelBlue, - /// - LightYellow, - /// - Lime, - /// - LimeGreen, - /// - Linen, - /// - Magenta, - /// - Maroon, - /// - MediumAquamarine, - /// - MediumBlue, - /// - MediumOrchid, - /// - MediumPurple, - /// - MediumSeaGreen, - /// - MediumSlateBlue, - /// - MediumSpringGreen, - /// - MediumTurquoise, - /// - MediumVioletRed, - /// - MidnightBlue, - /// - MintCream, - /// - MistyRose, - /// - Moccasin, - /// - NavajoWhite, - /// - Navy, - /// - Oldlace, - /// - Olive, - /// - Olivedrab, - /// - Orange, - /// - OrangeRed, - /// - Orchid, - /// - PaleGoldenrod, - /// - PaleGreen, - /// - PaleTurquoise, - /// - PaleVioletRed, - /// - Papayawhip, - /// - Peachpuff, - /// - Peru, - /// - Pink, - /// - Plum, - /// - PowderBlue, - /// - Purple, - /// - Red, - /// - RosyBrown, - /// - RoyalBlue, - /// - SaddleBrown, - /// - Salmon, - /// - SandyBrown, - /// - SeaGreen, - /// - Seashell, - /// - Sienna, - /// - Silver, - /// - SkyBlue, - /// - SlateBlue, - /// - SlateGray, - /// - SlateGrey, - /// - Snow, - /// - SpringGreen, - /// - SteelBlue, - /// - Tan, - /// - Teal, - /// - Thistle, - /// - Tomato, - /// - Turquoise, - /// - Violet, - /// - Wheat, - /// - White, - /// - WhiteSmoke, - /// - Yellow, - /// Yellow Green color - YellowGreen, -} - -impl IntoAttribute for Color { - fn into_attribute(self) -> Attribute { - Attribute::String(self.to_string().into()) - } - - fn into_attribute_boxed(self: Box) -> leptos::Attribute { - self.into_attribute() - } -} diff --git a/src/ng/column.rs b/src/ng/column.rs deleted file mode 100644 index 6116f2a..0000000 --- a/src/ng/column.rs +++ /dev/null @@ -1,95 +0,0 @@ -use leptos::*; - -use crate::ng::{use_frame, use_frames, Color, Frame}; - -/// Column of widgets. -#[component] -pub fn Column( - #[prop(optional)] rows: Option, - #[prop(optional)] border_width: i32, - #[prop(default = Color::Black)] border_color: Color, - #[prop(optional)] stretch: Vec, - #[prop(optional)] spacing: i32, - #[prop(optional)] padding: i32, - children: Children, -) -> impl IntoView { - if rows.is_none() && stretch.is_empty() { - logging::warn!("Column: either `rows` or `stretch` must be specified"); - } - - let stretch = if let Some(rows) = rows { - (0..rows).map(|i| *stretch.get(i).unwrap_or(&1)).collect() - } else { - stretch - }; - let denominator: i32 = stretch.iter().sum(); - let rows = stretch.len(); - - let f = use_frame(); - - let s = spacing * (rows as i32 - 1); - let x = f.x + border_width / 2; - let mut y = f.y + border_width / 2; - let width = f.width - border_width; - - let cells: Vec<_> = (0..rows) - .map(|i| { - let height = (f.height - border_width - s) * stretch[i] / denominator; - let cell = Frame { - x, - y, - width, - height, - }; - y += height + spacing; - cell - }) - .collect(); - - { - let frames = use_frames(); - let mut frames = frames.borrow_mut(); - for i in (0..rows).rev() { - let Frame { - x, - y, - width, - height, - } = cells[i]; - let frame = Frame { - x: x + padding, - y: y + padding, - width: width - 2 * padding, - height: height - 2 * padding, - }; - frames.push(frame); - } - } - - children() - .nodes - .into_iter() - .take(rows) - .enumerate() - .map(|(i, child)| { - let Frame { - x, - y, - width, - height, - } = cells[i]; - view! { - - {child} - } - }) - .collect_view() -} diff --git a/src/ng/grid.rs b/src/ng/grid.rs deleted file mode 100644 index 9c4f0f8..0000000 --- a/src/ng/grid.rs +++ /dev/null @@ -1,65 +0,0 @@ -use leptos::*; - -use crate::ng::{use_frame, use_frames, Color, Frame}; - -/// Grid layout widget. -#[component] -pub fn Grid( - #[prop(default = 1)] rows: usize, - #[prop(default = 1)] cols: usize, - #[prop(optional)] border_width: i32, - #[prop(default = Color::Black)] border_color: Color, - #[prop(optional)] spacing: i32, - #[prop(optional)] padding: i32, - children: Children, -) -> impl IntoView { - let f = use_frame(); - - let max = cols * rows; - - let cols = cols as i32; - let rows = rows as i32; - let hspacing = spacing * (cols - 1); - let vspacing = spacing * (rows - 1); - let width = (f.width - border_width - hspacing) / cols; - let height = (f.height - border_width - vspacing) / rows; - - { - let frames = use_frames(); - let mut frames = frames.borrow_mut(); - for i in (0..max).rev() { - let x = f.x + border_width / 2 + (width + spacing) * (i as i32 % cols); - let y = f.y + border_width / 2 + (height + spacing) * (i as i32 / cols); - let frame = Frame { - x: x + padding, - y: y + padding, - width: width - 2 * padding, - height: height - 2 * padding, - }; - frames.push(frame); - } - } - - children() - .nodes - .into_iter() - .take(max) - .enumerate() - .map(|(i, child)| { - let x = f.x + border_width / 2 + (width + spacing) * (i as i32 % cols); - let y = f.y + border_width / 2 + (height + spacing) * (i as i32 / cols); - view! { - - {child} - } - }) - .collect_view() -} diff --git a/src/ng/label.rs b/src/ng/label.rs deleted file mode 100644 index d3403e1..0000000 --- a/src/ng/label.rs +++ /dev/null @@ -1,44 +0,0 @@ -use leptos::*; - -use crate::ng::{use_frame, Align, Color, VAlign}; - -#[component] -pub fn Label( - #[prop(optional)] bold: bool, - #[prop(optional)] font: String, - #[prop(default = 48)] font_size: i32, - #[prop(default = Align::Center)] align: Align, - #[prop(default = VAlign::Middle)] valign: VAlign, - #[prop(default = Color::Black)] color: Color, - children: Children, -) -> impl IntoView { - let f = use_frame(); - - let (x, anchor) = match align { - Align::Left => (f.x, "start"), - Align::Center | Align::Fill => (f.x + f.width / 2, "middle"), - Align::Right => (f.x + f.width, "end"), - }; - let (y, baseline) = match valign { - VAlign::Top => (f.y, "hanging"), - VAlign::Middle | VAlign::Fill => ((f.y + f.height / 2), "central"), - VAlign::Bottom => (f.y + f.height, "text-top"), - }; - - view! { - - {children()} - - } -} diff --git a/src/ng/row.rs b/src/ng/row.rs deleted file mode 100644 index 3ebca58..0000000 --- a/src/ng/row.rs +++ /dev/null @@ -1,95 +0,0 @@ -use leptos::*; - -use crate::ng::{use_frame, use_frames, Color, Frame}; - -/// Row of widgets. -#[component] -pub fn Row( - #[prop(optional)] cols: Option, - #[prop(optional)] border_width: i32, - #[prop(default = Color::Black)] border_color: Color, - #[prop(optional)] stretch: Vec, - #[prop(optional)] spacing: i32, - #[prop(optional)] padding: i32, - children: Children, -) -> impl IntoView { - if cols.is_none() && stretch.is_empty() { - logging::warn!("Row: either `cols` or `stretch` must be specified"); - } - - let stretch = if let Some(cols) = cols { - (0..cols).map(|i| *stretch.get(i).unwrap_or(&1)).collect() - } else { - stretch - }; - let denominator: i32 = stretch.iter().sum(); - let cols = stretch.len(); - - let f = use_frame(); - - let s = spacing * (cols as i32 - 1); - let mut x = f.x + border_width / 2; - let y = f.y + border_width / 2; - let height = f.height - border_width; - - let cells: Vec<_> = (0..cols) - .map(|i| { - let width = (f.width - border_width - s) * stretch[i] / denominator; - let cell = Frame { - x, - y, - width, - height, - }; - x += width + spacing; - cell - }) - .collect(); - - { - let frames = use_frames(); - let mut frames = frames.borrow_mut(); - for i in (0..cols).rev() { - let Frame { - x, - y, - width, - height, - } = cells[i]; - let frame = Frame { - x: x + padding, - y: y + padding, - width: width - 2 * padding, - height: height - 2 * padding, - }; - frames.push(frame); - } - } - - children() - .nodes - .into_iter() - .take(cols) - .enumerate() - .map(|(i, child)| { - let Frame { - x, - y, - width, - height, - } = cells[i]; - view! { - - {child} - } - }) - .collect_view() -} diff --git a/src/ng/slide.rs b/src/ng/slide.rs deleted file mode 100644 index f6e3f1c..0000000 --- a/src/ng/slide.rs +++ /dev/null @@ -1,141 +0,0 @@ -use leptos::{ - ev::{resize, MouseEvent}, - svg::Svg, - *, -}; -use leptos_use::*; - -use crate::ng::{provide_frame, Color, Frame, Metadata, SvgFrame}; - -const WIDTH: i32 = 1920; -const HEIGHT: i32 = 1080; -const POINTER_SIZE: i32 = 72; -const SLIDE_MARGIN: i32 = 32; - -#[component] -pub fn Slide( - #[prop(default = WIDTH)] width: i32, - #[prop(default = HEIGHT)] height: i32, - #[prop(optional)] background_color: Color, - #[prop(optional)] background_image: String, - #[prop(optional)] pointer: MaybeSignal, - #[prop(optional)] blur: MaybeSignal, - #[prop(default = 15)] blur_radius: i32, - children: Children, -) -> impl IntoView { - let metadata = use_context::(); - let (slide_width, set_slide_width) = create_signal(crate::ng::calc_width(SLIDE_MARGIN)); - if metadata.is_none() { - // Standalone slide usage (not within a slideshow) - let _ = use_event_listener(window(), resize, move |_| { - set_slide_width.set(crate::ng::calc_width(SLIDE_MARGIN)); - }); - } - - let frame = Frame { - width, - height, - ..Default::default() - }; - provide_frame(frame); - - let view_box = format!("0 0 {width} {height}"); - - let svg_ref: NodeRef = create_node_ref(); - let (pointer_position, set_pointer_position) = create_signal((0, 0)); - let on_mousemove = move |e: MouseEvent| { - let mut px = e.offset_x(); - let mut py = e.offset_y(); - if let Some(svg) = svg_ref.get() { - px = px * WIDTH / svg.client_width(); - py = py * HEIGHT / svg.client_height(); - provide_context(SvgFrame { - width: WIDTH, - height: HEIGHT, - client_width: svg.client_width(), - client_height: svg.client_height(), - }); - } - set_pointer_position.set((px, py)); - }; - let (pointer_in, set_pointer_in) = create_signal(false); - let pointer_visible = move || pointer.get() && pointer_in.get(); - - let blur_style = move || { - let radius = if blur.get() { blur_radius } else { 0 }; - format!( - r#"-webkit-filter: blur({radius}px); - -moz-filter: blur({radius}px); - -ms-filter: blur({radius}px); - filter: blur({radius}px); transition: all .3s;"# - ) - }; - - let bg_style = if !background_image.is_empty() { - format!( - r#"background-image: url({background_image}); - background-size: cover; - background-position: center; - background-repeat: no-repeat;"# - ) - } else { - Default::default() - }; - - view! { -
- -
-
- - - {children()} - - - -
-
-
- } -} - -#[component] -fn Pointer(position: ReadSignal<(i32, i32)>, visible: F) -> impl IntoView -where - F: Fn() -> bool + 'static, -{ - view! { - - } -} diff --git a/src/ng/slideshow.rs b/src/ng/slideshow.rs deleted file mode 100644 index 5133dcd..0000000 --- a/src/ng/slideshow.rs +++ /dev/null @@ -1,180 +0,0 @@ -use leptos::{ - ev::{keydown, resize}, - *, -}; -use leptos_use::*; -use std::collections::BTreeSet; - -use crate::ng::Metadata; - -const BUTTON_COUNT: usize = 6; -const PAGINATION_HEIGHT: i32 = 88; - -#[component] -pub fn SlideShow( - #[prop(optional)] current: usize, - #[prop(optional)] teacher_mode: bool, - #[prop(optional)] pointer: bool, - children: Children, -) -> impl IntoView { - let page = create_rw_signal(current); - let children = children().nodes; - let count = children.len(); - - let _ = use_event_listener(document(), keydown, move |e| { - if e.key() == "ArrowLeft" { - if page.get() > 0 { - page.set(page.get() - 1); - } - } else if e.key() == "ArrowRight" && page.get() < count - 1 { - page.set(page.get() + 1); - } - }); - - let (width, set_width) = create_signal(crate::ng::calc_width(PAGINATION_HEIGHT)); - - let _ = use_event_listener(window(), resize, move |_| { - set_width.set(crate::ng::calc_width(PAGINATION_HEIGHT)); - }); - - let mut metadata = Metadata { - teacher_mode, - pointer, - ..Default::default() - }; - - let children = children - .into_iter() - .enumerate() - .map(|(i, child)| { - metadata.visible = i == page.get(); - provide_context(metadata); - view! { } - }) - .collect_view(); - - view! { -
0 { format!("{}px", width) } else { "100%".to_string() } - } - > - - - {children} -
- } -} - -#[component] -fn Pagination(page: RwSignal, count: usize) -> impl IntoView { - let mut prev = None; - let pages = page_list(page.get(), count) - .into_iter() - .map(|i| { - let view = view! { }; - prev = Some(i); - view - }) - .collect_view(); - - let on_prev = move |_| { - if page.get() > 0 { - page.set(page.get() - 1); - } - }; - let on_next = move |_| { - if page.get() < count - 1 { - page.set(page.get() + 1); - } - }; - - view! { -
- -
- } -} - -#[component] -fn PageButton(index: usize, prev: Option, current: RwSignal) -> impl IntoView { - view! { - <> - -
  • - - {index + 1} - -
  • - - } -} - -fn page_list(current: usize, count: usize) -> Vec { - if count <= BUTTON_COUNT { - (0..count).collect() - } else { - let mut pages: BTreeSet = [0].into(); - let mut add_page = |page| { - if page < count { - pages.insert(page); - } - }; - add_page(count - 1); - let center = if current == 0 { - 1 - } else if current == count - 1 { - count - 2 - } else { - current - }; - add_page(center - 1); - add_page(center); - add_page(center + 1); - pages.into_iter().collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn page_list_calc() { - let c = BUTTON_COUNT; - for i in 0..c { - assert_eq!(page_list(i, c), (0..c).collect::>()); - } - - let c = 2 * BUTTON_COUNT; - assert_eq!(page_list(0, c), vec![0, 1, 2, c - 1]); - assert_eq!(page_list(1, c), vec![0, 1, 2, c - 1]); - assert_eq!(page_list(2, c), vec![0, 1, 2, 3, c - 1]); - assert_eq!(page_list(3, c), vec![0, 2, 3, 4, c - 1]); - assert_eq!(page_list(c - 4, c), vec![0, c - 5, c - 4, c - 3, c - 1]); - assert_eq!(page_list(c - 3, c), vec![0, c - 4, c - 3, c - 2, c - 1]); - assert_eq!(page_list(c - 2, c), vec![0, c - 3, c - 2, c - 1]); - assert_eq!(page_list(c - 1, c), vec![0, c - 3, c - 2, c - 1]); - } -} diff --git a/src/ng/svg.rs b/src/ng/svg.rs deleted file mode 100644 index 92b5a08..0000000 --- a/src/ng/svg.rs +++ /dev/null @@ -1,104 +0,0 @@ -use leptos::*; - -use crate::ng::{use_frame, Align, Frame, VAlign}; - -struct SvgProperties { - width: i32, - height: i32, - align: Align, - valign: VAlign, - scale: f32, - flip_x: bool, - flip_y: bool, -} - -/// SVG widget. -#[component] -pub fn Svg( - width: i32, - height: i32, - #[prop(default = Align::Center)] align: Align, - #[prop(default = VAlign::Middle)] valign: VAlign, - #[prop(default = 1.0)] scale: f32, - #[prop(optional)] flip_x: bool, - #[prop(optional)] flip_y: bool, - children: Children, -) -> impl IntoView { - let f = use_frame(); - let props = SvgProperties { - width, - height, - align, - valign, - scale, - flip_x, - flip_y, - }; - let transform = calc_transform(&f, &props); - - view! { {children()} } -} - -/// SVG-from-file widget. -#[component] -pub fn SvgFile( - width: i32, - height: i32, - #[prop(default = Align::Center)] align: Align, - #[prop(default = VAlign::Middle)] valign: VAlign, - #[prop(default = 1.0)] scale: f32, - #[prop(optional)] flip_x: bool, - #[prop(optional)] flip_y: bool, - src: &'static str, -) -> impl IntoView { - let f = use_frame(); - let props = SvgProperties { - width, - height, - align, - valign, - scale, - flip_x, - flip_y, - }; - let transform = calc_transform(&f, &props); - - view! { } -} - -fn calc_transform(f: &Frame, props: &SvgProperties) -> String { - let scale = if matches!(props.align, Align::Fill) || matches!(props.valign, VAlign::Fill) { - let sx = f.width as f32 / props.width as f32; - let sy = f.height as f32 / props.height as f32; - sx.min(sy) - } else { - props.scale - }; - - let width = (scale * props.width as f32).round() as i32; - let height = (scale * props.height as f32).round() as i32; - - let mut x = match props.align { - Align::Left => f.x, - Align::Center | Align::Fill => f.x + (f.width - width) / 2, - Align::Right => f.x + f.width - width, - }; - let mut y = match props.valign { - VAlign::Top => f.y, - VAlign::Middle | VAlign::Fill => f.y + (f.height - height) / 2, - VAlign::Bottom => f.y + f.height - height, - }; - - let mut sx = scale; - let mut sy = scale; - - if props.flip_x { - sx = -sx; - x += width; - } - if props.flip_y { - sy = -sy; - y += height; - } - format!("translate({x} {y}) scale({sx} {sy})") -} diff --git a/src/ng/text.rs b/src/ng/text.rs deleted file mode 100644 index f1621e0..0000000 --- a/src/ng/text.rs +++ /dev/null @@ -1,271 +0,0 @@ -use leptos::*; -use std::rc::Rc; -use wasm_bindgen::JsValue; -use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, MouseEvent}; - -use crate::ng::{use_frame, Color, Frame, SvgFrame}; - -struct TextProperties<'a> { - bold: bool, - font_size: i32, - font: &'a str, - line_height: f32, - indent: f32, -} - -#[derive(Clone)] -struct Rect { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, -} - -struct Output { - pub words: Vec, - pub rects: Rc>, - pub letter_counters: Vec, -} - -/// Text widget. -#[component] -pub fn Text( - #[prop(optional)] bold: bool, - #[prop(default = 48)] font_size: i32, - #[prop(default = Color::Black)] color: Color, - #[prop(default = "sans-serif".to_string(), into)] font: String, - #[prop(default = 1.2)] line_height: f32, - #[prop(default = 1.4)] indent: f32, - #[prop(default = Color::PaleGreen)] marker_color: Color, - #[prop(optional)] lattice: bool, - #[prop(optional)] erase_top: f32, - #[prop(optional)] erase_bottom: f32, - #[prop(optional, into)] words_read: RwSignal, - #[prop(optional, into)] letters_read: RwSignal, - #[prop(optional, into)] letters_total: RwSignal, - children: Children, -) -> impl IntoView { - let props = TextProperties { - bold, - font_size, - font: &font, - line_height, - indent, - }; - let canvas = canvas_context(&props); - let children = children().nodes; - let f = use_frame(); - let Output { - words, - rects, - letter_counters, - } = wrap(&children, &canvas, &props, &f); - - letters_total.set(letter_counters.iter().sum()); - - let word = |i, r: &Rect, hidden| { - view! { - = words_read.get()).then_some("hidden") } - x=r.x + r.width / 2 - y=r.y + r.height / 2 - class:has-text-weight-bold=bold - text-anchor="middle" - dominant-baseline="central" - font-size=font_size - style=format!("font-family: {}", font) - fill=color - pointer-events="none" - > - {&words[i]} - - } - }; - - let erase = |r: &Rect| { - view! { - {(erase_top > 0.0) - .then(|| { - let h = (erase_top * r.height as f32).round() as i32; - view! { } - })} - - {(erase_bottom > 0.0) - .then(|| { - let h = (erase_bottom * r.height as f32).round() as i32; - view! { - - } - })} - } - }; - - let r = Rc::clone(&rects); - let on_click = move |e: MouseEvent| { - let svg: Option = use_context(); - if let Some(svg) = svg { - let x = e.offset_x() * svg.width / svg.client_width; - let y = e.offset_y() * svg.height / svg.client_height; - if x >= f.x && x <= f.x + f.width && y >= f.y && y <= f.y + f.height { - if let Some(index) = find_word_index(x, y, &r) { - words_read.set(index + 1); - letters_read.set(letter_counters[0..index + 1].iter().sum()); - } - } - } - }; - - let expand = text_width(" ", &canvas) / 2 + 1; - - view! { - - {rects.iter().enumerate().map(|(i, r)| word(i, r, false)).collect_view()} - {(erase_top > 0.0 || erase_bottom > 0.0) - .then(|| { rects.iter().map(erase).collect_view() })} - - {lattice - .then(|| { - let width = font_size / 2; - let dx = 5 * width; - let count = f.width / dx; - (0..count) - .map(|i| { - view! { - - } - }) - .collect_view() - })} - - {rects - .iter() - .enumerate() - .map(|(i, r)| { - view! { - = words_read.get()).then_some("hidden") } - x=r.x - expand - y=r.y - expand - width=r.width + 2 * expand - height=r.height + 2 * expand - rx=expand - ry=expand - fill=marker_color - pointer-events="none" - > - } - }) - .collect_view()} - - {rects.iter().enumerate().map(|(i, r)| word(i, r, true)).collect_view()} - } -} - -fn canvas_context(props: &TextProperties) -> CanvasRenderingContext2d { - let doc = web_sys::window() - .and_then(|win| win.document()) - .expect("Unable to get document"); - - let canvas = HtmlCanvasElement::from(JsValue::from(doc.create_element("canvas").unwrap())); - let context = - CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap())); - - let font_weight = if props.bold { 700 } else { 400 }; - context.set_font(&format!( - "{font_weight} {}px {}", - props.font_size, props.font - )); - context -} - -fn text_width(text: &str, canvas: &CanvasRenderingContext2d) -> i32 { - canvas.measure_text(text).unwrap().width() as i32 -} - -fn wrap( - children: &[View], - canvas: &CanvasRenderingContext2d, - props: &TextProperties, - frame: &Frame, -) -> Output { - let children = children.iter().map(|item| { - if let View::Text(text) = item { - text.content.clone() - } else { - Default::default() - } - }); - - let mut y = frame.y; - let mut words = Vec::new(); - let mut rects = Vec::new(); - let mut letter_counters = Vec::new(); - for child in children { - let mut sentence_words = child.split(' '); - let first_word = sentence_words.next().unwrap().to_string(); - letter_counters.push(first_word.chars().filter(|c| c.is_alphabetic()).count()); - - let mut indent = (props.indent * props.font_size as f32).round() as i32; - - let dy = (props.line_height * props.font_size as f32).round() as i32; - let mut x = frame.x + indent; - let mut lines = Vec::new(); - let mut line = first_word.clone(); - rects.push(Rect { - x, - y, - width: text_width(&first_word, canvas), - height: props.font_size, - }); - x += text_width(&first_word, canvas); - words.push(first_word); - for word in sentence_words { - words.push(word.to_string()); - letter_counters.push(word.chars().filter(|c| c.is_alphabetic()).count()); - if x + text_width(&format!(" {word}"), canvas) > frame.x + frame.width { - lines.push(line.clone()); - line = word.to_string(); - indent = 0; - x = frame.x; - y += dy; - } else { - line.push(' '); - x = frame.x + indent + text_width(&line, canvas); - line.push_str(word); - } - rects.push(Rect { - x, - y, - width: text_width(word, canvas), - height: props.font_size, - }); - x = frame.x + indent + text_width(&line, canvas); - } - if !line.is_empty() { - lines.push(line); - } - - y += dy; - } - Output { - rects: Rc::new(rects), - words, - letter_counters, - } -} - -fn find_word_index(x: i32, y: i32, rects: &[Rect]) -> Option { - rects - .iter() - .position(|r| x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) -} diff --git a/src/properties/mod.rs b/src/properties.rs similarity index 100% rename from src/properties/mod.rs rename to src/properties.rs diff --git a/src/properties/color.rs b/src/properties/color.rs index 00586fa..dade2e9 100644 --- a/src/properties/color.rs +++ b/src/properties/color.rs @@ -1,4 +1,5 @@ use derive_more::Display; +use leptos::{Attribute, IntoAttribute}; // TODO: Define all colors @@ -308,3 +309,13 @@ pub enum Color { /// Yellow Green color YellowGreen, } + +impl IntoAttribute for Color { + fn into_attribute(self) -> Attribute { + Attribute::String(self.to_string().into()) + } + + fn into_attribute_boxed(self: Box) -> leptos::Attribute { + self.into_attribute() + } +} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 9d14c01..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Utility functions and various helpers. - -use gloo_events::EventListener; -use rand::rngs::OsRng; -use std::collections::HashMap; -use wasm_bindgen::JsCast; -use yew::{html::Scope, prelude::*}; - -/// Creates Random Number Generator (RNG). -pub fn rng() -> OsRng { - Default::default() -} - -/// Add keyboard handler. -/// -/// `messages` is an assotiative array with the keyboard key as a key ansd a message as a value. -pub fn add_key_handler(link: &Scope, messages: HashMap) -where - T: Component, - M: Into + Copy + 'static, -{ - let doc = web_sys::window().and_then(|win| win.document()); - if let Some(doc) = doc { - let link = link.clone(); - let event = EventListener::new(&doc, "keydown", move |e| { - if let Some(e) = e.dyn_ref::() { - let key = e.key_code(); - if messages.contains_key(&key) { - link.send_message(messages[&key]); - } - } - }); - event.forget(); - } -} - -/// Add resize event handler. -pub fn add_resize_handler(link: &Scope, message: M) -where - T: Component, - M: Into + Copy + 'static, -{ - let win = web_sys::window(); - if let Some(win) = win { - let link = link.clone(); - let event = EventListener::new(&win, "resize", move |_| { - link.send_message(message); - }); - event.forget(); - } -} diff --git a/src/widgets.rs b/src/widgets.rs index 5507e71..73a0b6e 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -17,7 +17,7 @@ pub use label::Label; pub use row::Row; pub use slide::Slide; pub use slideshow::SlideShow; -pub use svg::Svg; +pub use svg::{Svg, SvgFile}; pub use text::Text; /// Additional information provided to all slides. @@ -31,19 +31,16 @@ pub struct Metadata { pub pointer: bool, } -/// Frame within which the widget will be rendered. -#[derive(Clone, Default, Debug, PartialEq)] -pub struct Frame { - /// X-coordinate (in pixels) of the to left corner. - pub x: i32, - /// Y-coordinate (in pixels) of the to left corner. - pub y: i32, - /// Width (in pixels). - pub width: i32, - /// Height (in pixels). - pub height: i32, - /// Screen X to SVG X transform factor. - pub fx: f32, - /// Screen Y to SVG Y transform factor. - pub fy: f32, +/// Calculates the width of the slide. +pub fn calc_width(margin: i32) -> i32 { + let elem = web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.document_element()); + if let Some(elem) = elem { + let width = elem.client_width(); + let height = elem.client_height(); + width.min((height - margin) * 16 / 9) + } else { + 0 + } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 93e3481..d2a35bc 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,124 +1,87 @@ -use yew::prelude::*; +use leptos::*; +use web_sys::MouseEvent; -use crate::{ - properties::{Align, Color, VAlign}, - widgets::{Frame, Label}, -}; +use crate::{provide_frame, use_frame, Align, Color, Frame, Label, VAlign}; const WIDTH: i32 = 400; const HEIGHT: i32 = 150; -/// Button properties. -#[derive(Default, Clone, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub text: String, - #[prop_or_default] - pub text_bold: bool, - #[prop_or_default] - pub html: Html, - #[prop_or(WIDTH)] - pub width: i32, - #[prop_or(HEIGHT)] - pub height: i32, - #[prop_or(24)] - pub radius: i32, - #[prop_or_default] - pub font: String, - #[prop_or(48)] - pub font_size: i32, - #[prop_or(Color::AliceBlue)] - pub color: Color, - #[prop_or(Color::Black)] - pub text_color: Color, - #[prop_or(12)] - pub border_width: i32, - #[prop_or(Color::RoyalBlue)] - pub border_color: Color, - #[prop_or(Align::Center)] - pub align: Align, - #[prop_or(VAlign::Middle)] - pub valign: VAlign, - #[prop_or_default] - pub onclick: Callback, -} - -/// Button widget. -#[function_component] -pub fn Button(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Button( + on_click: CB, + #[prop(optional)] text_bold: bool, + #[prop(default = WIDTH)] width: i32, + #[prop(default = HEIGHT)] height: i32, + #[prop(default = 24)] radius: i32, + #[prop(optional)] font: String, + #[prop(default = 48)] font_size: i32, + #[prop(default = Color::AliceBlue)] color: Color, + #[prop(default = Color::Black)] text_color: Color, + #[prop(default = 12)] border_width: i32, + #[prop(default = Color::RoyalBlue)] border_color: Color, + #[prop(default = Align::Center)] align: Align, + #[prop(default = VAlign::Middle)] valign: VAlign, + children: Children, +) -> impl IntoView +where + CB: FnMut(MouseEvent) + 'static, +{ + let f = use_frame(); - let width = if props.align == Align::Fill { - f.width - } else { - props.width - }; - let height = if props.valign == VAlign::Fill { + let width = if align == Align::Fill { f.width } else { width }; + let height = if valign == VAlign::Fill { f.height } else { - props.height + height }; - let x = match props.align { + let x = match align { Align::Left | Align::Fill => f.x, Align::Center => f.x + (f.width - width) / 2, Align::Right => f.x + f.width - width, }; - let y = match props.valign { + let y = match valign { VAlign::Top | VAlign::Fill => f.y, VAlign::Middle => f.y + (f.height - height) / 2, VAlign::Bottom => f.y + f.height - height, }; - let mouse_down = use_state(|| false); - let onmousedown = { - let mouse_down = mouse_down.clone(); - Callback::from(move |_| mouse_down.set(true)) - }; - let onmouseup = { - let mouse_down = mouse_down.clone(); - let props = props.clone(); - Callback::from(move |_| { - if *mouse_down { - mouse_down.set(false); - props.onclick.emit(props.clone()); - } - }) - }; - let onmouseleave = { - let mouse_down = mouse_down.clone(); - Callback::from(move |_| { - mouse_down.set(false); - }) - }; - - let border_width = if *mouse_down { - props.border_width + 6 - } else { - props.border_width - }; - let frame = Frame { x, y, width, height, - ..f }; - let x = (x + border_width / 2).to_string(); - let y = (y + border_width / 2).to_string(); - let width = (width - border_width).to_string(); - let height = (height - border_width).to_string(); - html! { - - - context={ frame }> - > - + provide_frame(frame); + + let x = x + border_width / 2; + let y = y + border_width / 2; + let width = width - border_width; + let height = height - border_width; + + let (border, set_border) = create_signal(border_width); + let on_mousedown = move |_| set_border.set(border_width + 6); + let on_mouseup = move |_| set_border.set(border_width); + + view! { + + } } diff --git a/src/widgets/column.rs b/src/widgets/column.rs index 24f7dea..bbe1682 100644 --- a/src/widgets/column.rs +++ b/src/widgets/column.rs @@ -1,57 +1,95 @@ -use yew::prelude::*; - -use crate::{properties::Color, widgets::Frame}; - -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or(0)] - pub border_width: i32, - #[prop_or(Color::Black)] - pub border_color: Color, - #[prop_or_default] - pub stretch: Vec, - #[prop_or_default] - pub spacing: i32, - #[prop_or_default] - pub padding: i32, -} +use leptos::*; + +use crate::{use_frame, use_frames, Color, Frame}; /// Column of widgets. -#[function_component] -pub fn Column(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Column( + #[prop(optional)] rows: Option, + #[prop(optional)] border_width: i32, + #[prop(default = Color::Black)] border_color: Color, + #[prop(optional)] stretch: Vec, + #[prop(optional)] spacing: i32, + #[prop(optional)] padding: i32, + children: Children, +) -> impl IntoView { + if rows.is_none() && stretch.is_empty() { + logging::warn!("Column: either `rows` or `stretch` must be specified"); + } - let stretch: Vec<_> = (0..props.children.len()) - .map(|i| *props.stretch.get(i).unwrap_or(&1)) - .collect(); + let stretch = if let Some(rows) = rows { + (0..rows).map(|i| *stretch.get(i).unwrap_or(&1)).collect() + } else { + stretch + }; let denominator: i32 = stretch.iter().sum(); + let rows = stretch.len(); - let spacing = props.spacing * (props.children.len() as i32 - 1); - let x = f.x + props.border_width / 2; - let mut y = f.y + props.border_width / 2; - let width = f.width - props.border_width; + let f = use_frame(); - html! { - for props.children.iter().enumerate().map(|(i, item)| { - let height = (f.height - props.border_width - spacing) * stretch[i] / denominator; - let frame = Frame { - x: x + props.padding, - y: y + props.padding, - width: width - 2 * props.padding, - height: height - 2 * props.padding, - ..f - }; - let html = html_nested! { - context={ frame }> - - { item } - > + let s = spacing * (rows as i32 - 1); + let x = f.x + border_width / 2; + let mut y = f.y + border_width / 2; + let width = f.width - border_width; + + let cells: Vec<_> = (0..rows) + .map(|i| { + let height = (f.height - border_width - s) * stretch[i] / denominator; + let cell = Frame { + x, + y, + width, + height, }; - y += height + props.spacing; - html + y += height + spacing; + cell }) + .collect(); + + { + let frames = use_frames(); + let mut frames = frames.borrow_mut(); + for i in (0..rows).rev() { + let Frame { + x, + y, + width, + height, + } = cells[i]; + let frame = Frame { + x: x + padding, + y: y + padding, + width: width - 2 * padding, + height: height - 2 * padding, + }; + frames.push(frame); + } } + + children() + .nodes + .into_iter() + .take(rows) + .enumerate() + .map(|(i, child)| { + let Frame { + x, + y, + width, + height, + } = cells[i]; + view! { + + {child} + } + }) + .collect_view() } diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index 1f65de1..8f4f89d 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -1,56 +1,65 @@ -use yew::prelude::*; +use leptos::*; -use crate::{properties::Color, widgets::Frame}; - -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or(1)] - pub rows: usize, - #[prop_or(1)] - pub cols: usize, - #[prop_or(0)] - pub border_width: i32, - #[prop_or(Color::Black)] - pub border_color: Color, - #[prop_or_default] - pub spacing: i32, - #[prop_or_default] - pub padding: i32, -} +use crate::{use_frame, use_frames, Color, Frame}; /// Grid layout widget. -#[function_component] -pub fn Grid(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Grid( + #[prop(default = 1)] rows: usize, + #[prop(default = 1)] cols: usize, + #[prop(optional)] border_width: i32, + #[prop(default = Color::Black)] border_color: Color, + #[prop(optional)] spacing: i32, + #[prop(optional)] padding: i32, + children: Children, +) -> impl IntoView { + let f = use_frame(); + + let max = cols * rows; - let cols = props.cols as i32; - let rows = props.rows as i32; - let hspacing = props.spacing * (cols - 1); - let vspacing = props.spacing * (rows - 1); - let width = (f.width - props.border_width - hspacing) / cols; - let height = (f.height - props.border_width - vspacing) / rows; + let cols = cols as i32; + let rows = rows as i32; + let hspacing = spacing * (cols - 1); + let vspacing = spacing * (rows - 1); + let width = (f.width - border_width - hspacing) / cols; + let height = (f.height - border_width - vspacing) / rows; - let max = props.cols * props.rows; - html! { - for props.children.iter().take(max).enumerate().map(|(i, item)| { - let x = f.x + props.border_width / 2 + (width + props.spacing) * (i as i32 % cols); - let y = f.y + props.border_width / 2 + (height + props.spacing) * (i as i32 / cols); + { + let frames = use_frames(); + let mut frames = frames.borrow_mut(); + for i in (0..max).rev() { + let x = f.x + border_width / 2 + (width + spacing) * (i as i32 % cols); + let y = f.y + border_width / 2 + (height + spacing) * (i as i32 / cols); let frame = Frame { - x: x + props.padding, - y: y + props.padding, - width: width - 2 * props.padding, - height: height - 2 * props.padding, - ..f + x: x + padding, + y: y + padding, + width: width - 2 * padding, + height: height - 2 * padding, }; - html_nested! { - context={ frame }> - - { item } - > + frames.push(frame); + } + } + + children() + .nodes + .into_iter() + .take(max) + .enumerate() + .map(|(i, child)| { + let x = f.x + border_width / 2 + (width + spacing) * (i as i32 % cols); + let y = f.y + border_width / 2 + (height + spacing) * (i as i32 / cols); + view! { + + {child} } }) - } + .collect_view() } diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 3b93369..a5f098a 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,60 +1,44 @@ -use yew::prelude::*; +use leptos::*; -use crate::{ - properties::{Align, Color, VAlign}, - widgets::Frame, -}; +use crate::{use_frame, Align, Color, VAlign}; -/// Label properties. -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub text: String, - #[prop_or_default] - pub bold: bool, - #[prop_or_default] - pub html: Html, - #[prop_or_default] - pub font: String, - #[prop_or(48)] - pub font_size: i32, - #[prop_or(Align::Center)] - pub align: Align, - #[prop_or(VAlign::Middle)] - pub valign: VAlign, - #[prop_or(Color::Black)] - pub color: Color, -} - -/// Label widget. -#[function_component] -pub fn Label(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Label( + #[prop(optional)] bold: bool, + #[prop(optional)] font: String, + #[prop(default = 48)] font_size: i32, + #[prop(default = Align::Center)] align: Align, + #[prop(default = VAlign::Middle)] valign: VAlign, + #[prop(default = Color::Black)] color: Color, + children: Children, +) -> impl IntoView { + let f = use_frame(); - let (x, anchor) = match props.align { + let (x, anchor) = match align { Align::Left => (f.x, "start"), Align::Center | Align::Fill => (f.x + f.width / 2, "middle"), Align::Right => (f.x + f.width, "end"), }; - let (y, baseline) = match props.valign { + let (y, baseline) = match valign { VAlign::Top => (f.y, "hanging"), VAlign::Middle | VAlign::Fill => ((f.y + f.height / 2), "central"), VAlign::Bottom => (f.y + f.height, "text-top"), }; - let class = classes!(props.bold.then_some("has-text-weight-bold")); - let style = classes!((!props.font.is_empty()).then(|| format!("font-family: {};", props.font))); - - html! { - - { - if props.text.is_empty() { - props.html.clone() - } else { - props.text.clone().into() - } - } + view! { + + {children()} } } diff --git a/src/widgets/row.rs b/src/widgets/row.rs index 3e9657e..84cc286 100644 --- a/src/widgets/row.rs +++ b/src/widgets/row.rs @@ -1,57 +1,95 @@ -use yew::prelude::*; - -use crate::{properties::Color, widgets::Frame}; - -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or(0)] - pub border_width: i32, - #[prop_or(Color::Black)] - pub border_color: Color, - #[prop_or_default] - pub stretch: Vec, - #[prop_or_default] - pub spacing: i32, - #[prop_or_default] - pub padding: i32, -} +use leptos::*; + +use crate::{use_frame, use_frames, Color, Frame}; /// Row of widgets. -#[function_component] -pub fn Row(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Row( + #[prop(optional)] cols: Option, + #[prop(optional)] border_width: i32, + #[prop(default = Color::Black)] border_color: Color, + #[prop(optional)] stretch: Vec, + #[prop(optional)] spacing: i32, + #[prop(optional)] padding: i32, + children: Children, +) -> impl IntoView { + if cols.is_none() && stretch.is_empty() { + logging::warn!("Row: either `cols` or `stretch` must be specified"); + } - let stretch: Vec<_> = (0..props.children.len()) - .map(|i| *props.stretch.get(i).unwrap_or(&1)) - .collect(); + let stretch = if let Some(cols) = cols { + (0..cols).map(|i| *stretch.get(i).unwrap_or(&1)).collect() + } else { + stretch + }; let denominator: i32 = stretch.iter().sum(); + let cols = stretch.len(); - let spacing = props.spacing * (props.children.len() as i32 - 1); - let mut x = f.x + props.border_width / 2; - let y = f.y + props.border_width / 2; - let height = f.height - props.border_width; + let f = use_frame(); - html! { - for props.children.iter().enumerate().map(|(i, item)| { - let width = (f.width - props.border_width - spacing) * stretch[i] / denominator; - let frame = Frame { - x: x + props.padding, - y: y + props.padding, - width: width - 2 * props.padding, - height: height - 2 * props.padding, - ..f - }; - let html = html_nested! { - context={ frame }> - - { item } - > + let s = spacing * (cols as i32 - 1); + let mut x = f.x + border_width / 2; + let y = f.y + border_width / 2; + let height = f.height - border_width; + + let cells: Vec<_> = (0..cols) + .map(|i| { + let width = (f.width - border_width - s) * stretch[i] / denominator; + let cell = Frame { + x, + y, + width, + height, }; - x += width + props.spacing; - html + x += width + spacing; + cell }) + .collect(); + + { + let frames = use_frames(); + let mut frames = frames.borrow_mut(); + for i in (0..cols).rev() { + let Frame { + x, + y, + width, + height, + } = cells[i]; + let frame = Frame { + x: x + padding, + y: y + padding, + width: width - 2 * padding, + height: height - 2 * padding, + }; + frames.push(frame); + } } + + children() + .nodes + .into_iter() + .take(cols) + .enumerate() + .map(|(i, child)| { + let Frame { + x, + y, + width, + height, + } = cells[i]; + view! { + + {child} + } + }) + .collect_view() } diff --git a/src/widgets/slide.rs b/src/widgets/slide.rs index 1d88905..35da06b 100644 --- a/src/widgets/slide.rs +++ b/src/widgets/slide.rs @@ -1,202 +1,141 @@ -use web_sys::SvgElement; -use yew::{prelude::*, ContextProvider}; +use leptos::{ + ev::{resize, MouseEvent}, + svg::Svg, + *, +}; +use leptos_use::*; -use crate::{properties::Color, utils, widgets::Frame}; +use crate::{provide_frame, Color, Frame, Metadata, SvgFrame}; const WIDTH: i32 = 1920; const HEIGHT: i32 = 1080; const POINTER_SIZE: i32 = 72; - -/// Slide widget. -#[derive(Clone, Default)] -pub struct Slide { - svg_ref: NodeRef, - pointer_x: i32, - pointer_y: i32, - width: i32, -} - -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or(WIDTH)] - pub width: i32, - #[prop_or(HEIGHT)] - pub height: i32, - #[prop_or_default] - pub defs: Html, - #[prop_or_default] - pub background_color: Color, - #[prop_or_default] - pub background_image: String, - #[prop_or_default] - pub pointer: bool, - #[prop_or_default] - pub blur: bool, - #[prop_or(15)] - pub blur_radius: i32, - #[prop_or_default] - pub onclick: Callback<(i32, i32)>, -} - -#[derive(Clone, Copy)] -pub enum Msg { - MovePointer { x: i32, y: i32 }, - HidePointer, - Clicked { x: i32, y: i32 }, - Resize, -} - -impl Component for Slide { - type Message = Msg; - type Properties = Props; - - fn create(ctx: &Context) -> Self { - utils::add_resize_handler(ctx.link(), Msg::Resize); - Self { - width: Self::calc_width(), - ..Default::default() - } - } - - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let p = ctx.props(); - match msg { - Msg::MovePointer { x, y } => { - if let Some(svg) = self.svg_ref.cast::() { - self.pointer_x = x * WIDTH / svg.client_width(); - self.pointer_y = y * HEIGHT / svg.client_height(); - } - true - } - Msg::HidePointer => { - self.pointer_x = 0; - self.pointer_y = 0; - true - } - Msg::Clicked { x, y } => { - if let Some(svg) = self.svg_ref.cast::() { - let x = x * WIDTH / svg.client_width(); - let y = y * HEIGHT / svg.client_height(); - p.onclick.emit((x, y)); - } - false - } - Msg::Resize => { - self.width = Self::calc_width(); - true - } - } - } - - fn view(&self, ctx: &Context) -> Html { - let p = ctx.props(); - let view_box = format!("0 0 {} {}", p.width, p.height); - - let onmousemove = ctx.link().callback(|e: MouseEvent| Msg::MovePointer { - x: e.offset_x(), - y: e.offset_y(), +const SLIDE_MARGIN: i32 = 32; + +#[component] +pub fn Slide( + #[prop(default = WIDTH)] width: i32, + #[prop(default = HEIGHT)] height: i32, + #[prop(optional)] background_color: Color, + #[prop(optional)] background_image: String, + #[prop(optional)] pointer: MaybeSignal, + #[prop(optional)] blur: MaybeSignal, + #[prop(default = 15)] blur_radius: i32, + children: Children, +) -> impl IntoView { + let metadata = use_context::(); + let (slide_width, set_slide_width) = create_signal(crate::calc_width(SLIDE_MARGIN)); + if metadata.is_none() { + // Standalone slide usage (not within a slideshow) + let _ = use_event_listener(window(), resize, move |_| { + set_slide_width.set(crate::calc_width(SLIDE_MARGIN)); }); + } - let onmouseleave = ctx.link().callback(|_| Msg::HidePointer); - - let onclick = ctx.link().callback(|e: MouseEvent| Msg::Clicked { - x: e.offset_x(), - y: e.offset_y(), - }); - - let mut fx = 0.0; - let mut fy = 0.0; - - if let Some(svg) = self.svg_ref.cast::() { - fx = WIDTH as f32 / svg.client_width() as f32; - fy = HEIGHT as f32 / svg.client_height() as f32; + let frame = Frame { + width, + height, + ..Default::default() + }; + provide_frame(frame); + + let view_box = format!("0 0 {width} {height}"); + + let svg_ref: NodeRef = create_node_ref(); + let (pointer_position, set_pointer_position) = create_signal((0, 0)); + let on_mousemove = move |e: MouseEvent| { + let mut px = e.offset_x(); + let mut py = e.offset_y(); + if let Some(svg) = svg_ref.get() { + px = px * WIDTH / svg.client_width(); + py = py * HEIGHT / svg.client_height(); + provide_context(SvgFrame { + width: WIDTH, + height: HEIGHT, + client_width: svg.client_width(), + client_height: svg.client_height(), + }); } - - let frame = Frame { - x: 0, - y: 0, - width: p.width, - height: p.height, - fx, - fy, - }; - - let style = if self.width > 0 { - format!("max-width: {}px;", self.width) - } else { - ("max-width: 100%").to_string() - }; - - let radius = if p.blur { p.blur_radius } else { 0 }; - let blur = format!( + set_pointer_position.set((px, py)); + }; + let (pointer_in, set_pointer_in) = create_signal(false); + let pointer_visible = move || pointer.get() && pointer_in.get(); + + let blur_style = move || { + let radius = if blur.get() { blur_radius } else { 0 }; + format!( r#"-webkit-filter: blur({radius}px); - -moz-filter: blur({radius}px); - -ms-filter: blur({radius}px); - filter: blur({radius}px); transition: all .3s;"#, - ); - - let svg_style = if !p.background_image.is_empty() { - format!( - r#"background-image: url({}); - background-size: cover; - background-position: center; - background-repeat: no-repeat;"#, - &p.background_image - ) - } else { - Default::default() - }; - - html! { -
    -
    -
    - - { p.defs.clone() } - - { - for p.children.iter().map(|item|{ - html_nested! { - context={ frame.clone() }> - { item } - > - } - }) - } - { self.pointer_view(p.pointer) } - -
    -
    + -moz-filter: blur({radius}px); + -ms-filter: blur({radius}px); + filter: blur({radius}px); transition: all .3s;"# + ) + }; + + let bg_style = if !background_image.is_empty() { + format!( + r#"background-image: url({background_image}); + background-size: cover; + background-position: center; + background-repeat: no-repeat;"# + ) + } else { + Default::default() + }; + + view! { +
    + +
    +
    + + + {children()} + + + +
    - } +
    } } -impl Slide { - fn pointer_view(&self, pointer: bool) -> Html { - if pointer && self.pointer_x > 0 && self.pointer_y > 0 { - html_nested! { - - } - } else { - html_nested!() - } - } - - fn calc_width() -> i32 { - let elem = web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.document_element()); - if let Some(elem) = elem { - let width = elem.client_width(); - let height = elem.client_height(); - width.min((height - 88) * 16 / 9) - } else { - 0 - } +#[component] +fn Pointer(position: ReadSignal<(i32, i32)>, visible: F) -> impl IntoView +where + F: Fn() -> bool + 'static, +{ + view! { + } } diff --git a/src/widgets/slideshow.rs b/src/widgets/slideshow.rs index f3fd4e7..37d2717 100644 --- a/src/widgets/slideshow.rs +++ b/src/widgets/slideshow.rs @@ -1,223 +1,180 @@ -use std::collections::{BTreeSet, HashMap}; -use yew::{html::Scope, prelude::*}; +use leptos::{ + ev::{keydown, resize}, + *, +}; +use leptos_use::*; +use std::collections::BTreeSet; -use crate::{keys, utils, widgets::Metadata}; +use crate::Metadata; const BUTTON_COUNT: usize = 6; - -/// Set of slides that are to be displayed sequentially. -#[derive(Clone, Default)] -pub struct SlideShow { - current: usize, - count: usize, - width: i32, -} - -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or_default] - pub current: usize, - #[prop_or_default] - pub teacher_mode: bool, - #[prop_or_default] - pub pointer: bool, -} - -#[derive(Clone, Copy)] -pub enum Msg { - Prev, - Next, - SetCurrent(usize), - Resize, -} - -impl Component for SlideShow { - type Message = Msg; - type Properties = Props; - - fn create(ctx: &Context) -> Self { - let p = ctx.props(); - let link = ctx.link(); - let mut messages: HashMap<_, _> = [ - (keys::ARROW_LEFT, Msg::Prev), - (keys::ARROW_RIGHT, Msg::Next), - ] - .into(); - for k in keys::DIGIT_1..=keys::DIGIT_9 { - messages.insert(k, Msg::SetCurrent((k - keys::DIGIT_1) as _)); - } - utils::add_key_handler(link, messages); - utils::add_resize_handler(link, Msg::Resize); - - let count = p.children.len(); - let current = p.current.min(count - 1); - Self { - current, - count, - width: Self::calc_width(), - } - } - - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - let max = self.count - 1; - match msg { - Msg::Prev if self.current > 0 => self.current -= 1, - Msg::Next if self.current < max => self.current += 1, - Msg::SetCurrent(c) if c <= max => self.current = c, - Msg::Resize => self.width = Self::calc_width(), - _ => return false, - } - true - } - - fn view(&self, ctx: &Context) -> Html { - let p = ctx.props(); - let link = ctx.link(); - - let style = if self.width > 0 { - format!("max-width: {}px;", self.width) - } else { - ("max-width: 100%").to_string() - }; - - html! { - <> -
    - { self.pagination(link) } -
    - { - for p.children.iter().enumerate().map(|(i, item)| { - let metadata = Metadata { - visible: i == self.current, - teacher_mode: p.teacher_mode, - pointer: p.pointer, - }; - html_nested! { - - } - }) - } - +const PAGINATION_HEIGHT: i32 = 88; + +#[component] +pub fn SlideShow( + #[prop(optional)] current: usize, + #[prop(optional)] teacher_mode: bool, + #[prop(optional)] pointer: bool, + children: Children, +) -> impl IntoView { + let page = create_rw_signal(current); + let children = children().nodes; + let count = children.len(); + + let _ = use_event_listener(document(), keydown, move |e| { + if e.key() == "ArrowLeft" { + if page.get() > 0 { + page.set(page.get() - 1); + } + } else if e.key() == "ArrowRight" && page.get() < count - 1 { + page.set(page.get() + 1); } + }); + + let (width, set_width) = create_signal(crate::calc_width(PAGINATION_HEIGHT)); + + let _ = use_event_listener(window(), resize, move |_| { + set_width.set(crate::calc_width(PAGINATION_HEIGHT)); + }); + + let mut metadata = Metadata { + teacher_mode, + pointer, + ..Default::default() + }; + + let children = children + .into_iter() + .enumerate() + .map(|(i, child)| { + metadata.visible = i == page.get(); + provide_context(metadata); + view! { } + }) + .collect_view(); + + view! { +
    0 { format!("{}px", width) } else { "100%".to_string() } + } + > + + + {children} +
    } } -impl SlideShow { - fn page_list(current: usize, count: usize) -> Vec { - if count <= BUTTON_COUNT { - (0..count).collect() - } else { - let mut pages: BTreeSet = [0].into(); - let mut add_page = |page| { - if page < count { - pages.insert(page); - } - }; - add_page(count - 1); - let center = if current == 0 { - 1 - } else if current == count - 1 { - count - 2 - } else { - current - }; - add_page(center - 1); - add_page(center); - add_page(center + 1); - pages.into_iter().collect() +#[component] +fn Pagination(page: RwSignal, count: usize) -> impl IntoView { + let mut prev = None; + let pages = page_list(page.get(), count) + .into_iter() + .map(|i| { + let view = view! { }; + prev = Some(i); + view + }) + .collect_view(); + + let on_prev = move |_| { + if page.get() > 0 { + page.set(page.get() - 1); } - } - - fn page_button(&self, prev: Option, index: usize, scope: &Scope) -> Html { - let class = if index == self.current { - "pagination-link button is-warning" - } else { - "pagination-link" - }; - - let button = html! { -
  • - { index + 1 } -
  • - }; - - if matches!(prev, Some(p) if index != (p + 1)) { - html!(<>
  • { '•' }
  • { button }) - } else { - button + }; + let on_next = move |_| { + if page.get() < count - 1 { + page.set(page.get() + 1); } - } - - fn pagination(&self, scope: &Scope) -> Html { - let pages = Self::page_list(self.current, self.count); - let mut prev = None; + }; - html! { + view! { +
    - } +
    } +} - fn calc_width() -> i32 { - let elem = web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.document_element()); - if let Some(elem) = elem { - let width = elem.client_width(); - let height = elem.client_height(); - width.min((height - 88) * 16 / 9) +#[component] +fn PageButton(index: usize, prev: Option, current: RwSignal) -> impl IntoView { + view! { + <> + +
  • + + {index + 1} + +
  • + + } +} + +fn page_list(current: usize, count: usize) -> Vec { + if count <= BUTTON_COUNT { + (0..count).collect() + } else { + let mut pages: BTreeSet = [0].into(); + let mut add_page = |page| { + if page < count { + pages.insert(page); + } + }; + add_page(count - 1); + let center = if current == 0 { + 1 + } else if current == count - 1 { + count - 2 } else { - 0 - } + current + }; + add_page(center - 1); + add_page(center); + add_page(center + 1); + pages.into_iter().collect() } } #[cfg(test)] mod tests { - use super::{SlideShow, BUTTON_COUNT}; + use super::*; #[test] - fn page_list() { + fn page_list_calc() { let c = BUTTON_COUNT; for i in 0..c { - assert_eq!(SlideShow::page_list(i, c), (0..c).collect::>()); + assert_eq!(page_list(i, c), (0..c).collect::>()); } let c = 2 * BUTTON_COUNT; - assert_eq!(SlideShow::page_list(0, c), vec![0, 1, 2, c - 1]); - assert_eq!(SlideShow::page_list(1, c), vec![0, 1, 2, c - 1]); - assert_eq!(SlideShow::page_list(2, c), vec![0, 1, 2, 3, c - 1]); - assert_eq!(SlideShow::page_list(3, c), vec![0, 2, 3, 4, c - 1]); - assert_eq!( - SlideShow::page_list(c - 4, c), - vec![0, c - 5, c - 4, c - 3, c - 1] - ); - assert_eq!( - SlideShow::page_list(c - 3, c), - vec![0, c - 4, c - 3, c - 2, c - 1] - ); - assert_eq!(SlideShow::page_list(c - 2, c), vec![0, c - 3, c - 2, c - 1]); - assert_eq!(SlideShow::page_list(c - 1, c), vec![0, c - 3, c - 2, c - 1]); + assert_eq!(page_list(0, c), vec![0, 1, 2, c - 1]); + assert_eq!(page_list(1, c), vec![0, 1, 2, c - 1]); + assert_eq!(page_list(2, c), vec![0, 1, 2, 3, c - 1]); + assert_eq!(page_list(3, c), vec![0, 2, 3, 4, c - 1]); + assert_eq!(page_list(c - 4, c), vec![0, c - 5, c - 4, c - 3, c - 1]); + assert_eq!(page_list(c - 3, c), vec![0, c - 4, c - 3, c - 2, c - 1]); + assert_eq!(page_list(c - 2, c), vec![0, c - 3, c - 2, c - 1]); + assert_eq!(page_list(c - 1, c), vec![0, c - 3, c - 2, c - 1]); } } diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index afd3802..269ce76 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1,34 +1,72 @@ -use yew::prelude::*; +use leptos::*; -use crate::{ - properties::{Align, VAlign}, - widgets::Frame, -}; +use crate::{use_frame, Align, Frame, VAlign}; -/// SVG properties. -#[derive(Default, Clone, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - pub width: i32, - pub height: i32, - #[prop_or(Align::Center)] - pub align: Align, - #[prop_or(VAlign::Middle)] - pub valign: VAlign, - #[prop_or(1.0)] - pub scale: f32, - #[prop_or_default] - pub flip_x: bool, - #[prop_or_default] - pub flip_y: bool, +struct SvgProperties { + width: i32, + height: i32, + align: Align, + valign: VAlign, + scale: f32, + flip_x: bool, + flip_y: bool, } /// SVG widget. -#[function_component] -pub fn Svg(props: &Props) -> Html { - let f = use_context::().unwrap(); +#[component] +pub fn Svg( + width: i32, + height: i32, + #[prop(default = Align::Center)] align: Align, + #[prop(default = VAlign::Middle)] valign: VAlign, + #[prop(default = 1.0)] scale: f32, + #[prop(optional)] flip_x: bool, + #[prop(optional)] flip_y: bool, + children: Children, +) -> impl IntoView { + let f = use_frame(); + let props = SvgProperties { + width, + height, + align, + valign, + scale, + flip_x, + flip_y, + }; + let transform = calc_transform(&f, &props); + + view! { {children()} } +} + +/// SVG-from-file widget. +#[component] +pub fn SvgFile( + width: i32, + height: i32, + #[prop(default = Align::Center)] align: Align, + #[prop(default = VAlign::Middle)] valign: VAlign, + #[prop(default = 1.0)] scale: f32, + #[prop(optional)] flip_x: bool, + #[prop(optional)] flip_y: bool, + src: &'static str, +) -> impl IntoView { + let f = use_frame(); + let props = SvgProperties { + width, + height, + align, + valign, + scale, + flip_x, + flip_y, + }; + let transform = calc_transform(&f, &props); + view! { } +} + +fn calc_transform(f: &Frame, props: &SvgProperties) -> String { let scale = if matches!(props.align, Align::Fill) || matches!(props.valign, VAlign::Fill) { let sx = f.width as f32 / props.width as f32; let sy = f.height as f32 / props.height as f32; @@ -62,10 +100,5 @@ pub fn Svg(props: &Props) -> Html { sy = -sy; y += height; } - - html! { - - { for props.children.iter() } - - } + format!("translate({x} {y}) scale({sx} {sy})") } diff --git a/src/widgets/text.rs b/src/widgets/text.rs index e8c6b98..e6d9e7b 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -1,21 +1,21 @@ +extern crate alloc; + +use alloc::rc::Rc; +use leptos::*; use wasm_bindgen::JsValue; -use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; -use yew::{prelude::*, virtual_dom::VNode}; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, MouseEvent}; -use crate::{properties::Color, widgets::Frame}; +use crate::{use_frame, Color, Frame, SvgFrame}; -/// Text widget. -pub struct Text { - frame: Frame, - canvas: CanvasRenderingContext2d, - words: Vec, - letter_counters: Vec, - total_letters: usize, - rects: Vec, - expand: i32, - _context_listener: ContextHandle, +struct TextProperties<'a> { + bold: bool, + font_size: i32, + font: &'a str, + line_height: f32, + indent: f32, } +#[derive(Clone)] struct Rect { pub x: i32, pub y: i32, @@ -23,289 +23,251 @@ struct Rect { pub height: i32, } -/// Text properties. -#[derive(Clone, Default, Properties, PartialEq)] -pub struct Props { - #[prop_or_default] - pub children: Children, - #[prop_or_default] - pub bold: bool, - #[prop_or(48)] - pub font_size: i32, - #[prop_or(Color::Black)] - pub color: Color, - #[prop_or_else(|| "sans-serif".to_string())] - pub font: String, - #[prop_or(1.2)] - pub line_height: f32, - #[prop_or(1.4)] - pub indent: f32, - #[prop_or(Color::PaleGreen)] - pub marker_color: Color, - #[prop_or_default] - pub words_read: usize, - #[prop_or_default] - pub lattice: bool, - #[prop_or_default] - pub erase_top: f32, - #[prop_or_default] - pub erase_bottom: f32, - #[prop_or_default] - pub onread: Callback<(usize, usize, usize)>, -} - -pub enum Msg { - ContextUpdated(Frame), - Clicked(i32, i32), +struct Output { + pub words: Vec, + pub rects: Rc>, + pub letter_counters: Vec, } -impl Component for Text { - type Message = Msg; - type Properties = Props; +/// Text widget. +#[component] +pub fn Text( + #[prop(optional)] bold: bool, + #[prop(default = 48)] font_size: i32, + #[prop(default = Color::Black)] color: Color, + #[prop(default = "sans-serif".to_string(), into)] font: String, + #[prop(default = 1.2)] line_height: f32, + #[prop(default = 1.4)] indent: f32, + #[prop(default = Color::PaleGreen)] marker_color: Color, + #[prop(optional)] lattice: bool, + #[prop(optional)] erase_top: f32, + #[prop(optional)] erase_bottom: f32, + #[prop(optional, into)] words_read: RwSignal, + #[prop(optional, into)] letters_read: RwSignal, + #[prop(optional, into)] letters_total: RwSignal, + children: Children, +) -> impl IntoView { + let props = TextProperties { + bold, + font_size, + font: &font, + line_height, + indent, + }; + let canvas = canvas_context(&props); + let children = children().nodes; + let f = use_frame(); + let Output { + words, + rects, + letter_counters, + } = wrap(&children, &canvas, &props, &f); - fn create(ctx: &Context) -> Self { - let p = ctx.props(); - let (frame, _context_listener) = ctx - .link() - .context(ctx.link().callback(Msg::ContextUpdated)) - .expect("No context provided"); + letters_total.set(letter_counters.iter().sum()); - let canvas = Self::canvas_context(p); + let word = |i, r: &Rect, hidden| { + view! { + = words_read.get()).then_some("hidden") } + x=r.x + r.width / 2 + y=r.y + r.height / 2 + class:has-text-weight-bold=bold + text-anchor="middle" + dominant-baseline="central" + font-size=font_size + style=format!("font-family: {}", font) + fill=color + pointer-events="none" + > + {&words[i]} + + } + }; - let expand = Self::text_width(" ", &canvas) / 2 + 1; + let erase = |r: &Rect| { + view! { + {(erase_top > 0.0) + .then(|| { + let h = (erase_top * r.height as f32).round() as i32; + view! { } + })} - let mut text = Self { - frame, - canvas, - words: Default::default(), - letter_counters: Default::default(), - total_letters: Default::default(), - rects: Default::default(), - expand, - _context_listener, - }; - text.wrap(p); - let letters = text.letter_counters.iter().take(p.words_read).sum(); - p.onread.emit((p.words_read, letters, text.total_letters)); - text - } + {(erase_bottom > 0.0) + .then(|| { + let h = (erase_bottom * r.height as f32).round() as i32; + view! { + + } + })} + } + }; - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let p = ctx.props(); - match msg { - Msg::ContextUpdated(frame) => { - self.frame = frame; - true - } - Msg::Clicked(x, y) => { - if let Some(index) = self.find_word_index(x, y) { - let words_read = index + 1; - let letters = self.letter_counters.iter().take(words_read).sum(); - p.onread.emit((words_read, letters, self.total_letters)); - true - } else { - false + let r = Rc::clone(&rects); + let on_click = move |e: MouseEvent| { + let svg: Option = use_context(); + if let Some(svg) = svg { + let x = e.offset_x() * svg.width / svg.client_width; + let y = e.offset_y() * svg.height / svg.client_height; + if x >= f.x && x <= f.x + f.width && y >= f.y && y <= f.y + f.height { + if let Some(index) = find_word_index(x, y, &r) { + words_read.set(index + 1); + letters_read.set(letter_counters[0..index + 1].iter().sum()); } } } - } - - fn view(&self, ctx: &Context) -> Html { - let p = ctx.props(); - let f = &self.frame; - - let onclick = { - let fx = f.fx; - let fy = f.fy; - ctx.link().callback(move |e: MouseEvent| { - let x = (e.offset_x() as f32 * fx).round() as i32; - let y = (e.offset_y() as f32 * fy).round() as i32; - Msg::Clicked(x, y) - }) - }; + }; - let class = if p.bold { "has-text-weight-bold" } else { "" }; + let expand = text_width(" ", &canvas) / 2 + 1; - let lattice = if p.lattice { - let width = p.font_size / 2; - let dx = 5 * width; - let count = f.width / dx; - html! { - for (0..count).map(|i| html_nested!()) - } - } else { - Default::default() - }; - - let word = |(i, r): (usize, &Rect)| { - html_nested! { - - { self.words[i].clone() } - - } - }; - - let erase = |r: &Rect| { - let erase_top = if p.erase_top > 0.0 { - let h = (p.erase_top * r.height as f32).round() as i32; - html_nested!() - } else { - Default::default() - }; - let erase_bottom = if p.erase_bottom > 0.0 { - let h = (p.erase_bottom * r.height as f32).round() as i32; - html_nested!() - } else { - Default::default() - }; - html_nested! { - <> - { erase_top } - { erase_bottom } - - } - }; + view! { + + {rects.iter().enumerate().map(|(i, r)| word(i, r, false)).collect_view()} + {(erase_top > 0.0 || erase_bottom > 0.0) + .then(|| { rects.iter().map(erase).collect_view() })} - html! { - <> - - { for self.rects.iter().enumerate().map(word) } - { - if p.erase_top > 0.0 || p.erase_bottom > 0.0 { - self.rects.iter().map(erase).collect::() - } else { - Default::default() - } - } - { lattice } - { - for self.rects.iter().take(p.words_read).map(|r| { - html! { - + {lattice + .then(|| { + let width = font_size / 2; + let dx = 5 * width; + let count = f.width / dx; + (0..count) + .map(|i| { + view! { + } }) + .collect_view() + })} + + {rects + .iter() + .enumerate() + .map(|(i, r)| { + view! { + = words_read.get()).then_some("hidden") } + x=r.x - expand + y=r.y - expand + width=r.width + 2 * expand + height=r.height + 2 * expand + rx=expand + ry=expand + fill=marker_color + pointer-events="none" + > } - { for self.rects.iter().take(p.words_read).enumerate().map(word) } - - } + }) + .collect_view()} + + {rects.iter().enumerate().map(|(i, r)| word(i, r, true)).collect_view()} } } -impl Text { - fn canvas_context(props: &Props) -> CanvasRenderingContext2d { - let doc = web_sys::window() - .and_then(|win| win.document()) - .expect("Unable to get document"); +fn canvas_context(props: &TextProperties) -> CanvasRenderingContext2d { + let doc = web_sys::window() + .and_then(|win| win.document()) + .expect("Unable to get document"); - let canvas = HtmlCanvasElement::from(JsValue::from(doc.create_element("canvas").unwrap())); - let context = CanvasRenderingContext2d::from(JsValue::from( - canvas.get_context("2d").unwrap().unwrap(), - )); + let canvas = HtmlCanvasElement::from(JsValue::from(doc.create_element("canvas").unwrap())); + let context = + CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap())); - let font_weight = if props.bold { 700 } else { 400 }; - context.set_font(&format!( - "{font_weight} {}px {}", - props.font_size, props.font - )); - context - } + let font_weight = if props.bold { 700 } else { 400 }; + context.set_font(&format!( + "{font_weight} {}px {}", + props.font_size, props.font + )); + context +} - fn text_width(text: &str, canvas: &CanvasRenderingContext2d) -> i32 { - canvas.measure_text(text).unwrap().width() as i32 - } +fn text_width(text: &str, canvas: &CanvasRenderingContext2d) -> i32 { + canvas.measure_text(text).unwrap().width() as i32 +} - fn wrap(&mut self, props: &Props) { - let children = props.children.iter().map(|item| { - if let VNode::VText(node) = item { - node.text - } else { - Default::default() - } - }); +fn wrap( + children: &[View], + canvas: &CanvasRenderingContext2d, + props: &TextProperties, + frame: &Frame, +) -> Output { + let children = children.iter().map(|item| { + if let View::Text(text) = item { + text.content.clone() + } else { + Default::default() + } + }); - let mut y = self.frame.y; - for child in children { - let mut words = child.split(' '); - let first_word = words.next().unwrap().to_string(); - self.letter_counters - .push(first_word.chars().filter(|c| c.is_alphabetic()).count()); + let mut y = frame.y; + let mut words = Vec::new(); + let mut rects = Vec::new(); + let mut letter_counters = Vec::new(); + for child in children { + let mut sentence_words = child.split(' '); + let first_word = sentence_words.next().unwrap().to_string(); + letter_counters.push(first_word.chars().filter(|c| c.is_alphabetic()).count()); - let mut indent = (props.indent * props.font_size as f32).round() as i32; + let mut indent = (props.indent * props.font_size as f32).round() as i32; - let dy = (props.line_height * props.font_size as f32).round() as i32; - let mut x = self.frame.x + indent; - let mut lines = Vec::new(); - let mut line = first_word.clone(); - self.rects.push(Rect { + let dy = (props.line_height * props.font_size as f32).round() as i32; + let mut x = frame.x + indent; + let mut lines = Vec::new(); + let mut line = first_word.clone(); + rects.push(Rect { + x, + y, + width: text_width(&first_word, canvas), + height: props.font_size, + }); + x += text_width(&first_word, canvas); + words.push(first_word); + for word in sentence_words { + words.push(word.to_string()); + letter_counters.push(word.chars().filter(|c| c.is_alphabetic()).count()); + if x + text_width(&format!(" {word}"), canvas) > frame.x + frame.width { + lines.push(line.clone()); + line = word.to_string(); + indent = 0; + x = frame.x; + y += dy; + } else { + line.push(' '); + x = frame.x + indent + text_width(&line, canvas); + line.push_str(word); + } + rects.push(Rect { x, y, - width: Self::text_width(&first_word, &self.canvas), + width: text_width(word, canvas), height: props.font_size, }); - x += Self::text_width(&first_word, &self.canvas); - self.words.push(first_word); - for word in words { - self.words.push(word.to_string()); - self.letter_counters - .push(word.chars().filter(|c| c.is_alphabetic()).count()); - if x + Self::text_width(&format!(" {word}"), &self.canvas) - > self.frame.x + self.frame.width - { - lines.push(line.clone()); - line = word.to_string(); - indent = 0; - x = self.frame.x; - y += dy; - } else { - line.push(' '); - x = self.frame.x + indent + Self::text_width(&line, &self.canvas); - line.push_str(word); - } - self.rects.push(Rect { - x, - y, - width: Self::text_width(word, &self.canvas), - height: props.font_size, - }); - x = self.frame.x + indent + Self::text_width(&line, &self.canvas); - } - if !line.is_empty() { - lines.push(line); - } - - y += dy; + x = frame.x + indent + text_width(&line, canvas); } - self.total_letters = self.letter_counters.iter().sum(); - } - - fn find_word_index(&self, x: i32, y: i32) -> Option { - if x < self.frame.x - || x > self.frame.x + self.frame.width - || y < self.frame.y - || y > self.frame.y + self.frame.height - { - return None; + if !line.is_empty() { + lines.push(line); } - self.rects - .iter() - .enumerate() - .find(|(_, r)| x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) - .map(|(i, _)| i) + + y += dy; } + Output { + rects: Rc::new(rects), + words, + letter_counters, + } +} + +fn find_word_index(x: i32, y: i32, rects: &[Rect]) -> Option { + rects + .iter() + .position(|r| x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) }