Skip to main content

rustolio_web/elements/
mod.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11pub mod html;
12
13use std::rc::Rc;
14
15pub mod html_tags {
16    // container macros
17    // Not included: body, html, head, meta, link
18    pub use rustolio_web_core_macros::*;
19}
20
21#[derive(Debug, Clone)]
22pub struct Element {
23    pub(crate) inner: InnerElement,
24    pub(crate) attributes: html::Attributes,
25}
26
27#[derive(Debug, Clone)]
28pub(crate) enum InnerElement {
29    HtmlTag {
30        tag: &'static str,
31        children: Elements,
32        namespace: Option<&'static str>,
33    },
34    Text(Rc<str>),
35    Raw {
36        raw: &'static str,
37        supported_type: web_sys::SupportedType,
38    },
39    None,
40}
41
42impl Default for Element {
43    fn default() -> Self {
44        Element::empty()
45    }
46}
47
48impl Element {
49    pub fn html(tag: &'static str, attributes: html::Attributes, children: Elements) -> Self {
50        Self {
51            inner: InnerElement::HtmlTag {
52                tag,
53                children,
54                namespace: None,
55            },
56            attributes,
57        }
58    }
59
60    pub fn svg(tag: &'static str, attributes: html::Attributes, children: Elements) -> Self {
61        Self {
62            inner: InnerElement::HtmlTag {
63                tag,
64                children,
65                namespace: Some("http://www.w3.org/2000/svg"),
66            },
67            attributes,
68        }
69    }
70
71    pub fn text(text: impl ToString) -> Self {
72        Self {
73            inner: InnerElement::Text(text.to_string().into()),
74            attributes: html::Attributes::default(),
75        }
76    }
77
78    fn text_rc(rc: Rc<str>) -> Self {
79        Self {
80            inner: InnerElement::Text(rc),
81            attributes: html::Attributes::default(),
82        }
83    }
84
85    pub fn raw(
86        attributes: html::Attributes,
87        raw: &'static str,
88        supported_type: web_sys::SupportedType,
89    ) -> Self {
90        Self {
91            inner: InnerElement::Raw {
92                raw,
93                supported_type,
94            },
95            attributes,
96        }
97    }
98
99    pub fn option(option: Option<impl Into<Element>>) -> Self {
100        match option {
101            Some(e) => e.into(),
102            None => Element::empty(),
103        }
104    }
105
106    pub fn result(result: crate::Result<impl Into<Element>>) -> Self {
107        crate::error::match_result("Element", result.map(|e| e.into()))
108    }
109
110    pub fn empty() -> Self {
111        Self {
112            inner: InnerElement::None,
113            attributes: html::Attributes::default(),
114        }
115    }
116
117    pub fn is_empty(&self) -> bool {
118        match &self.inner {
119            InnerElement::None => true,
120            InnerElement::Text(s) => s.trim().is_empty(),
121            _ => false,
122        }
123    }
124
125    /// Indicates whether attributes or listener can be set on this element.
126    /// (E.g. a text-node cannot have those)
127    pub fn is_html(&self) -> bool {
128        !matches!(&self.inner, InnerElement::Text(_))
129    }
130
131    pub fn non_empty(self) -> Option<Self> {
132        if self.is_empty() {
133            return None;
134        }
135        Some(self)
136    }
137
138    pub fn add_attribute(
139        &mut self,
140        name: &'static str,
141        value: impl Into<html::AttributeValue>,
142    ) -> bool {
143        if !self.is_html() {
144            return false;
145        }
146        self.attributes.add_attribute(name, value);
147        true
148    }
149
150    pub fn add_listener(&mut self, listener: &html::Listener) -> bool {
151        if !self.is_html() {
152            return false;
153        }
154        self.attributes.add_listener(listener);
155        true
156    }
157}
158
159pub struct ElementWrapper(Element);
160pub trait IntoElementWrapper {
161    fn into_wrapper(self) -> ElementWrapper;
162}
163impl<T> IntoElementWrapper for T
164where
165    T: Into<Element>,
166{
167    fn into_wrapper(self) -> ElementWrapper {
168        ElementWrapper(self.into())
169    }
170}
171impl From<ElementWrapper> for Element {
172    #[inline]
173    fn from(val: ElementWrapper) -> Self {
174        val.0
175    }
176}
177impl From<ElementWrapper> for Elements {
178    #[inline]
179    fn from(val: ElementWrapper) -> Self {
180        Elements::single(val.0)
181    }
182}
183impl Element {
184    #[inline]
185    pub fn into_wrapper(self) -> ElementWrapper {
186        ElementWrapper(self)
187    }
188}
189
190pub struct StringWrapper(Rc<str>);
191impl From<StringWrapper> for Element {
192    #[inline]
193    fn from(val: StringWrapper) -> Self {
194        Element::text_rc(val.0)
195    }
196}
197impl From<StringWrapper> for Elements {
198    #[inline]
199    fn from(val: StringWrapper) -> Self {
200        Elements::single(Element::text_rc(val.0))
201    }
202}
203pub trait IntoStringWrapper {
204    fn into_wrapper(&self) -> StringWrapper;
205}
206macro_rules! impl_from_tostring {
207    ($($impl_ty:ty)*) => {
208        $(
209            impl IntoStringWrapper for $impl_ty {
210                fn into_wrapper(&self) -> StringWrapper {
211                    StringWrapper(self.to_string().into())
212                }
213            }
214            impl From<$impl_ty> for Element {
215                fn from(value: $impl_ty) -> Self {
216                    Element::text(value)
217                }
218            }
219        )*
220    };
221}
222impl_from_tostring! { u8 u16 u32 u64 u128 usize }
223impl_from_tostring! { i8 i16 i32 i64 i128 isize }
224impl_from_tostring! { f32 f64 }
225impl_from_tostring! { &str char String &String }
226impl_from_tostring! { bool }
227
228impl IntoStringWrapper for Rc<str> {
229    fn into_wrapper(&self) -> StringWrapper {
230        StringWrapper(self.clone())
231    }
232}
233impl From<Rc<str>> for Element {
234    fn from(value: Rc<str>) -> Self {
235        Element::text_rc(value)
236    }
237}
238
239impl<T> From<Option<T>> for Element
240where
241    T: Into<Element>,
242{
243    #[inline]
244    fn from(val: Option<T>) -> Self {
245        Element::option(val)
246    }
247}
248impl<T> From<crate::Result<T>> for Element
249where
250    T: Into<Element>,
251{
252    #[inline]
253    fn from(val: crate::Result<T>) -> Self {
254        Element::result(val)
255    }
256}
257
258use wasm_bindgen::prelude::*;
259#[wasm_bindgen]
260#[derive(Debug, Clone)]
261pub struct Elements {
262    pub(crate) inner: InnerElements,
263}
264
265#[derive(Clone)]
266pub(crate) enum InnerElements {
267    Element(Rc<Element>),
268    Elements(Rc<[Elements]>),
269    Overlay(Rc<dyn Fn() -> Element>),
270    Dynamic(Rc<dyn Fn() -> Element>),
271}
272
273impl std::fmt::Debug for InnerElements {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        match self {
276            Self::Element(e) => f.debug_tuple("Element").field(e).finish(),
277            Self::Elements(e) => f.debug_tuple("Elements").field(e).finish(),
278            Self::Overlay(_) => f.debug_tuple("Overlay").field(&"Funtion").finish(),
279            Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"Function").finish(),
280        }
281    }
282}
283
284impl Default for Elements {
285    fn default() -> Self {
286        Self::empty()
287    }
288}
289
290impl Elements {
291    pub fn single(element: impl Into<Element>) -> Self {
292        Self {
293            inner: InnerElements::Element(Rc::new(element.into())),
294        }
295    }
296
297    pub fn iter(iter: impl IntoIterator<Item = Elements>) -> Self {
298        Self {
299            inner: InnerElements::Elements(iter.into_iter().collect()),
300        }
301    }
302
303    pub fn option(option: Option<impl Into<Elements>>) -> Self {
304        match option {
305            Some(e) => e.into(),
306            None => Elements::empty(),
307        }
308    }
309
310    pub fn result(result: crate::Result<impl Into<Elements>>) -> Self {
311        crate::error::match_result("Element", result.map(|e| e.into()))
312    }
313
314    pub fn dynamic(f: impl Fn() -> Element + 'static) -> Self {
315        Self {
316            inner: InnerElements::Dynamic(Rc::new(f)),
317        }
318    }
319
320    pub fn overlay(f: impl Fn() -> Element + 'static) -> Self {
321        Self {
322            inner: InnerElements::Overlay(Rc::new(f)),
323        }
324    }
325
326    pub fn empty() -> Self {
327        Self {
328            inner: InnerElements::Element(Rc::new(Element::empty())),
329        }
330    }
331
332    pub fn is_empty(&self) -> bool {
333        match &self.inner {
334            InnerElements::Dynamic(_) | InnerElements::Overlay(_) => false,
335            InnerElements::Element(e) => e.is_empty(),
336            InnerElements::Elements(v) => v.is_empty() && v.iter().all(|e| e.is_empty()),
337        }
338    }
339
340    pub fn non_empty(self) -> Option<Self> {
341        if self.is_empty() {
342            return None;
343        }
344        Some(self)
345    }
346}
347
348impl IntoIterator for Elements {
349    type IntoIter = ElementsIter;
350    type Item = Elements;
351
352    fn into_iter(self) -> Self::IntoIter {
353        if let InnerElements::Elements(elements) = self.inner {
354            return ElementsIter {
355                elements: None,
356                elements_iter: elements
357                    .iter()
358                    .cloned()
359                    .collect::<Vec<_>>()
360                    .into_iter(),
361            };
362        }
363        ElementsIter {
364            elements: Some(self),
365            elements_iter: Vec::new().into_iter(),
366        }
367    }
368}
369pub struct ElementsIter {
370    elements: Option<Elements>,
371    elements_iter: <Vec<Elements> as IntoIterator>::IntoIter,
372}
373impl Iterator for ElementsIter {
374    type Item = Elements;
375    fn next(&mut self) -> Option<Self::Item> {
376        if let Some(elements) = self.elements_iter.next() {
377            return Some(elements);
378        }
379        let Some(elements) = self.elements.take() else {
380            return None;
381        };
382        if elements.is_empty() {
383            return None;
384        }
385        Some(elements)
386    }
387}
388
389impl From<Element> for Elements {
390    #[inline]
391    fn from(val: Element) -> Self {
392        Elements::single(val)
393    }
394}
395
396pub struct ElementsWrapper(Elements);
397impl From<ElementsWrapper> for Elements {
398    #[inline]
399    fn from(val: ElementsWrapper) -> Self {
400        val.0
401    }
402}
403impl Elements {
404    #[inline]
405    pub fn into_wrapper(self) -> ElementsWrapper {
406        ElementsWrapper(self)
407    }
408}
409pub trait IntoElementsWrapper {
410    fn into_wrapper(self) -> ElementsWrapper;
411}
412impl<T> IntoElementsWrapper for crate::Result<T>
413where
414    T: Into<Elements>,
415{
416    fn into_wrapper(self) -> ElementsWrapper {
417        ElementsWrapper(Elements::result(self))
418    }
419}
420
421// Element Fn
422pub struct ElementClosureWrapper(Rc<dyn Fn() -> Element>);
423impl From<ElementClosureWrapper> for Elements {
424    #[inline]
425    fn from(val: ElementClosureWrapper) -> Self {
426        Elements {
427            inner: InnerElements::Dynamic(val.0),
428        }
429    }
430}
431pub trait IntoElementClosureWrapper {
432    fn into_wrapper(self) -> ElementClosureWrapper;
433}
434impl<F, E> IntoElementClosureWrapper for F
435where
436    F: Fn() -> E + 'static,
437    E: Into<Element>,
438{
439    #[inline]
440    fn into_wrapper(self) -> ElementClosureWrapper {
441        ElementClosureWrapper(Rc::new(move || self().into()))
442    }
443}
444// pub trait IntoElementResultClosureWrapper {
445//     fn into_wrapper(self) -> ElementClosureWrapper;
446// }
447// impl<F, E> IntoElementResultClosureWrapper for F
448// where
449//     F: Fn() -> crate::Result<E> + 'static,
450//     E: Into<Element>,
451// {
452//     #[inline]
453//     fn into_wrapper(self) -> ElementClosureWrapper {
454//         ElementClosureWrapper(Rc::new(crate::error::convert_fn_0(
455//             "Dynamic Element",
456//             move || self().map(|e| e.into()),
457//         )))
458//     }
459// }
460
461// String Fn
462// pub trait IntoStringFnWrapper {
463//     fn into_wrapper(self) -> ElementFnWrapper;
464// }
465// impl<F, E> IntoStringFnWrapper for F
466// where
467//     F: Fn() -> E + 'static,
468//     E: ToString,
469// {
470//     #[inline]
471//     fn into_wrapper(self) -> ElementFnWrapper {
472//         ElementFnWrapper(Rc::new(move || Element::text(self().to_string())))
473//     }
474// }
475
476// Element Iter
477pub struct ElementsIteratorWrapper(Rc<[Elements]>);
478impl From<ElementsIteratorWrapper> for Elements {
479    #[inline]
480    fn from(val: ElementsIteratorWrapper) -> Self {
481        Elements {
482            inner: InnerElements::Elements(val.0),
483        }
484    }
485}
486pub trait IntoElementIteratorWrapper {
487    #[allow(private_interfaces)]
488    fn into_wrapper(self) -> ElementsIteratorWrapper;
489}
490impl<I, Item> IntoElementIteratorWrapper for I
491where
492    I: Iterator<Item = Item>,
493    Item: Into<Element>,
494{
495    #[allow(private_interfaces)]
496    #[inline]
497    fn into_wrapper(self) -> ElementsIteratorWrapper {
498        ElementsIteratorWrapper(self.map(|i| Elements::single(i)).collect())
499    }
500}
501// pub trait IntoResultElementIteratorWrapper {
502//     #[allow(private_interfaces)]
503//     fn into_wrapper(self) -> ElementsIteratorWrapper;
504// }
505// impl<I, Item> IntoResultElementIteratorWrapper for I
506// where
507//     I: Iterator<Item = crate::Result<Item>>,
508//     Item: Into<Element>,
509// {
510//     #[allow(private_interfaces)]
511//     #[inline]
512//     fn into_wrapper(self) -> ElementsIteratorWrapper {
513//         ElementsIteratorWrapper(match_result(
514//             "Element Iterator",
515//             self.map(|i| Ok::<_, crate::Error>(Elements::single(i?)))
516//                 .collect(),
517//         ))
518//     }
519// }
520
521// Elements Iter
522pub trait IntoElementsIteratorWrapper {
523    fn into_wrapper(self) -> ElementsIteratorWrapper;
524}
525impl<I> IntoElementsIteratorWrapper for I
526where
527    I: Iterator<Item = Elements>,
528{
529    #[inline]
530    fn into_wrapper(self) -> ElementsIteratorWrapper {
531        ElementsIteratorWrapper(self.collect())
532    }
533}
534// pub trait IntoResultElementsIteratorWrapper {
535//     #[allow(private_interfaces)]
536//     fn into_wrapper(self) -> ElementsIteratorWrapper;
537// }
538// impl<I> IntoResultElementsIteratorWrapper for I
539// where
540//     I: Iterator<Item = crate::Result<Elements>>,
541// {
542//     #[allow(private_interfaces)]
543//     #[inline]
544//     fn into_wrapper(self) -> ElementsIteratorWrapper {
545//         ElementsIteratorWrapper(match_result("Element Iterator", self.collect()))
546//     }
547// }
548
549// Rc<[Elements]>
550pub trait RcElementsSliceWrapper {
551    fn into_wrapper(self) -> ElementsIteratorWrapper;
552}
553impl RcElementsSliceWrapper for Rc<[Elements]> {
554    #[inline]
555    fn into_wrapper(self) -> ElementsIteratorWrapper {
556        ElementsIteratorWrapper(self)
557    }
558}
559
560// Rc<[Element]>
561pub trait RcElementSliceWrapper {
562    fn into_wrapper(self) -> ElementsIteratorWrapper;
563}
564impl RcElementSliceWrapper for Rc<[Element]> {
565    #[inline]
566    fn into_wrapper(self) -> ElementsIteratorWrapper {
567        ElementsIteratorWrapper(
568            self.iter()
569                .map(|e| Elements::single(e.clone()))
570                .collect(),
571        )
572    }
573}
574
575// String Iter
576// pub trait IntoStringIteratorWrapper {
577//     #[allow(private_interfaces)]
578//     fn into_wrapper(self) -> ElementsIteratorWrapper;
579// }
580// impl<I> IntoStringIteratorWrapper for I
581// where
582//     I: Iterator + 'static,
583//     I::Item: ToString,
584// {
585//     #[allow(private_interfaces)]
586//     #[inline]
587//     fn into_wrapper(self) -> ElementsIteratorWrapper {
588//         ElementsIteratorWrapper(self.into_iter().map(|i| i.into_wrapper().into()).collect())
589//     }
590// }
591
592impl<T> From<Option<T>> for Elements
593where
594    T: Into<Elements>,
595{
596    #[inline]
597    fn from(val: Option<T>) -> Self {
598        Elements::option(val)
599    }
600}
601impl<T> From<crate::Result<T>> for Elements
602where
603    T: Into<Elements>,
604{
605    #[inline]
606    fn from(val: crate::Result<T>) -> Self {
607        Elements::result(val)
608    }
609}
610
611#[cfg(test)]
612#[allow(unused)]
613mod tests {
614    use crate::prelude::*;
615
616    fn raw_html() {
617        Element::html(
618            "div",
619            html::Attributes::new(),
620            elements![
621                String::new(),
622                Element::html("div", html::Attributes::new(), elements![]),
623                (|| String::new()),
624                [String::new()].into_iter().map(|i| i),
625                (vec![1].into_iter()),
626            ],
627        );
628    }
629
630    fn html() -> Element {
631        HtmlComponentWithOptional::builder().f2(None).f5("").build();
632        div! {
633            a! {},
634            Some(a! {}),
635            None::<Element>,
636            td! {
637                "Test"
638            },
639            span! {
640                p! {
641                    "Nested"
642                }
643            },
644            div! {
645                p! {},
646                "Mixed",
647            },
648            Some("tet"),
649            div! {
650                Element::option(Some(a! {})),
651            },
652            div! {
653                "Mixed",
654                p! {},
655                html_fn(),
656                HtmlComponentWithAttributes {
657                    f1: String::new(),
658                    with:class: "test",
659                    with:class: || "test",
660                    with:disabled: true,
661                    with:selected: || true,
662                    on:click: |_: Event| {},
663                },
664                HtmlComponentWithChildren {
665                    f1: String::new(),
666                    "Test"
667                },
668                HtmlComponentWithChildren {
669                    f1: String::new(),
670                    html_fn()
671                },
672                HtmlComponentWithAttributesAndChildren {
673                    f1: String::new(),
674                },
675                HtmlComponentWithAttributesAndChildren {
676                    f1: String::new(),
677                    with:class: "Test",
678                    "Test"
679                },
680                HtmlComponentWithGenerics::<_, _, 4, _> {
681                    f1: &String::from(""),
682                    f2: String::new(),
683                    f3: String::new()
684                },
685                crate::elements::tests::HtmlComponent {},
686                HtmlComponentWithChild {
687                    f1: "",
688                    "",
689                },
690            },
691            div! {
692                id: "id",
693                on:click: move |_: Event| {},
694                class: "only attributes",
695            },
696            div! {
697                id: "id",
698                class: "attributes",
699                "and Text"
700            },
701            div! {
702                id: "id",
703                class: "attributes",
704                "and Text"
705            },
706            div! {
707                id: "id",
708                class: "attributes",
709                crate::elements::tests::HtmlComponent {},
710            },
711            elements! {},
712            elements! {
713                "Test"
714            },
715            elements! {
716                div! {}
717            },
718            elements! {
719                [1, 2, 3].into_iter()
720            },
721            elements! {
722                std::iter::once(a! {})
723            },
724            elements! {
725                "Test",
726                html_fn(),
727                div! {},
728                1..4,
729                (1..6).map(|i| div! { i }),
730                1..,
731                [div! {}, a! {}].into_iter(),
732                [
733                    Ok(div! {}),
734                    Ok(a! {}),
735                    Err(crate::Error::new("ERROR")),
736                ].into_iter(),
737                move || a! {},
738                move || Ok(a! {}),
739                move || Err::<Element, _>(crate::Error::new("ERROR")),
740            },
741        }
742    }
743
744    fn html_fn() -> Element {
745        div! {
746            with:class: "test",
747            on:click: |_: Event| {},
748            "Test"
749        }
750    }
751
752    #[component]
753    fn HtmlComponent() -> Element {
754        Element::text("")
755    }
756
757    #[component]
758    fn ResultHtmlComponent() -> crate::Result<Element> {
759        Ok(Element::text(""))
760    }
761
762    #[component]
763    fn HtmlComponentWithAttributes(f1: String, attributes: html::Attributes) -> Element {
764        div! {
765            html_fn()
766        }
767    }
768
769    #[component]
770    fn HtmlComponentWithChildren(f1: String, children: Elements) -> Element {
771        div! {
772            html_fn()
773        }
774    }
775
776    #[component]
777    fn HtmlComponentWithChild(#[prop(into)] f1: String, children: Element) -> Element {
778        div! {
779            html_fn()
780        }
781    }
782
783    #[component]
784    fn HtmlComponentWithAttributesAndChildren(
785        f1: String,
786        attributes: html::Attributes,
787        children: Elements,
788    ) -> Element {
789        div! {
790            html_fn()
791        }
792    }
793
794    #[component]
795    fn HtmlComponentWithGenerics<'a, G: ToString, G1, const N: usize>(
796        f1: &'a G,
797        f2: impl Clone,
798        f3: G1,
799    ) -> Element
800    where
801        G1: Default,
802    {
803        f1.to_string();
804        f2.clone();
805        G1::default();
806        div! {
807            html_fn()
808        }
809    }
810
811    #[component]
812    fn HtmlComponentWithOptional(
813        f1: Option<String>,
814        #[prop(required, into)] f2: Option<String>,
815        #[prop(default)] f3: String,
816        #[prop(default = String::from("default"), into)] f4: String,
817        #[prop(into)] f5: String,
818    ) -> Element {
819        let s = ToStringStruct {
820            f1: String::from("test"),
821        };
822        div! {
823            "",
824            false,
825            || "",
826            || true,
827            s.to_string(),
828            move || s.clone().to_string(),
829        }
830    }
831
832    #[derive(Debug, Clone)]
833    struct ToStringStruct {
834        f1: String,
835    }
836    impl std::fmt::Display for ToStringStruct {
837        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838            write!(f, "self:?")
839        }
840    }
841}