Skip to main content

cotis_web/rendering/
drawable_defaults.rs

1//! Default [`HTMLDrawable`] implementations for
2//! [`cotis_defaults::render_commands`] types.
3//!
4//! These impls map layout output to CSS on shared DOM host elements (`cotis-{id}`).
5//! Multiple drawables with the same command id accumulate styles on one node via
6//! [`HTMLCanvas::ensure_command_element`](crate::rendering::HTMLCanvas::ensure_command_element).
7//!
8//! # Supported commands
9//!
10//! | Command | DOM host | Key CSS |
11//! |---------|----------|---------|
12//! | [`Rectangle`] | `cotis-{id}`, tag from [`ExtraHTMLData`](crate::custom_data::ExtraHTMLData) | `position:absolute`, `background-color`, `border-radius` |
13//! | [`Border`] | same | per-side `border-*: Npx solid` |
14//! | [`Text`] | same | `font-family: font{id}`, `color`, flex, `white-space: pre-wrap` |
15//! | [`Image`] | same | `background-image: url(...)`, `background-size: 100% 100%` |
16//! | [`ClipStart`] / [`ClipEnd`] | clip container stack | `overflow: hidden`, rounded clip |
17//! | [`RenderTList`] | (recursive) | dispatches `Type` / `List` variants |
18
19use crate::color::*;
20use crate::custom_data::OptionalExtraHTMLData;
21use crate::images::URLImage;
22use crate::rendering::{HTMLCanvas, HTMLDrawable};
23use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
24use cotis_defaults::render_commands::*;
25
26impl<T: HTMLDrawable, L: IsRenderList + HTMLDrawable> HTMLDrawable for RenderTList<T, L> {
27    fn draw(self, canvas: &mut HTMLCanvas) {
28        match self {
29            RenderTList::Type(t) => {
30                t.draw(canvas);
31            }
32            RenderTList::List(l) => {
33                l.draw(canvas);
34            }
35        }
36    }
37}
38
39impl<T: HTMLDrawable> HTMLDrawable for RenderTList<T, ()> {
40    fn draw(self, canvas: &mut HTMLCanvas) {
41        match self {
42            RenderTList::Type(t) => t.draw(canvas),
43            RenderTList::List(()) => {}
44        }
45    }
46}
47
48impl<'a, ExtraData: OptionalExtraHTMLData> HTMLDrawable for Rectangle<'a, ExtraData> {
49    fn draw(self, canvas: &mut HTMLCanvas) {
50        let extra_data = match self.info.extra_data {
51            None => Default::default(),
52            Some(extra) => extra.as_ref().extra_html_data().unwrap_or_default(),
53        };
54        canvas.ensure_command_element(
55            self.info.id,
56            extra_data.tag.as_json_str(),
57            extra_data.extra_style.as_deref(),
58        );
59        let bg = rectangle_fill_css(&self.color);
60        canvas
61            .current_element_css_push(&format!(
62                "position: absolute; left: {}px; top: {}px; width: {}px; height: {}px",
63                self.info.bounding_box.x,
64                self.info.bounding_box.y,
65                self.info.bounding_box.width,
66                self.info.bounding_box.height
67            ))
68            .current_element_css_push(&format!("background-color: {}", bg))
69            .current_element_css_push(&format!(
70                "border-radius: {}px {}px {}px {}px",
71                self.corner_radii.top_left,
72                self.corner_radii.top_right,
73                self.corner_radii.bottom_right,
74                self.corner_radii.bottom_left
75            ))
76            .current_element_css_push("box-sizing: border-box; pointer-events: auto");
77    }
78}
79
80impl<'a, ExtraData: OptionalExtraHTMLData> HTMLDrawable for Border<'a, ExtraData> {
81    fn draw(self, canvas: &mut HTMLCanvas) {
82        let extra_data = match self.info.extra_data {
83            None => Default::default(),
84            Some(extra) => extra.as_ref().extra_html_data().unwrap_or_default(),
85        };
86        canvas.ensure_command_element(
87            self.info.id,
88            extra_data.tag.as_json_str(),
89            extra_data.extra_style.as_deref(),
90        );
91        let border_color = color_css(&self.color);
92        let bb = self.info.bounding_box;
93        canvas
94            .current_element_css_push(&format!(
95                "position: absolute; left: {}px; top: {}px; width: {}px; height: {}px",
96                bb.x, bb.y, bb.width, bb.height
97            ))
98            .current_element_css_push(&format!(
99                "border-top: {}px solid {}",
100                self.width.top, border_color
101            ))
102            .current_element_css_push(&format!(
103                "border-right: {}px solid {}",
104                self.width.right, border_color
105            ))
106            .current_element_css_push(&format!(
107                "border-bottom: {}px solid {}",
108                self.width.bottom, border_color
109            ))
110            .current_element_css_push(&format!(
111                "border-left: {}px solid {}",
112                self.width.left, border_color
113            ))
114            .current_element_css_push(&format!(
115                "border-radius: {}px {}px {}px {}px",
116                self.corner_radii.top_left,
117                self.corner_radii.top_right,
118                self.corner_radii.bottom_right,
119                self.corner_radii.bottom_left
120            ))
121            .current_element_css_push("box-sizing: border-box; pointer-events: auto");
122    }
123}
124
125impl<'a, ExtraData: OptionalExtraHTMLData> HTMLDrawable for ClipStart<'a, ExtraData> {
126    fn draw(self, canvas: &mut HTMLCanvas) {
127        let bb = self.info.bounding_box;
128        canvas.push_scissor(
129            self.info.id,
130            bb.x,
131            bb.y,
132            bb.width,
133            bb.height,
134            self.corner_radii,
135        );
136    }
137}
138
139impl HTMLDrawable for ClipEnd {
140    fn draw(self, canvas: &mut HTMLCanvas) {
141        canvas.pop_scissor();
142    }
143}
144
145impl<'a, ExtraData: OptionalExtraHTMLData> HTMLDrawable for Text<'a, ExtraData> {
146    fn draw(self, canvas: &mut HTMLCanvas) {
147        let extra_data = match self.info.extra_data {
148            None => Default::default(),
149            Some(extra) => extra.as_ref().extra_html_data().unwrap_or_default(),
150        };
151        canvas.ensure_command_element(
152            self.info.id,
153            extra_data.tag.as_json_str(),
154            extra_data.extra_style.as_deref(),
155        );
156        let text_color = text_color_css(&self.color);
157        let font_size = self.font_size * 1.0;
158        let line_height = if self.line_height > 0.0 {
159            self.line_height
160        } else {
161            1.2
162        };
163        let bb = self.info.bounding_box;
164        canvas.set_text_content(self.text.as_ref().to_string());
165        canvas
166            .current_element_css_push(&format!(
167                "position: absolute; left: {}px; top: {}px; width: {}px; height: {}px",
168                bb.x, bb.y, bb.width, bb.height
169            ))
170            .current_element_css_push(&format!(
171                "font-family: font{}; font-size: {}px; line-height: {}; letter-spacing: {}px",
172                self.font_id, font_size, line_height, self.letter_spacing
173            ))
174            .current_element_css_push(&format!("color: {}", text_color))
175            .current_element_css_push("display: flex; align-items: flex-start")
176            .current_element_css_push(
177                "white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere",
178            )
179            .current_element_css_push("overflow: hidden; box-sizing: border-box")
180            .current_element_css_push("pointer-events: auto; user-select: text");
181    }
182}
183
184impl<'a, ImageData: URLImage, ExtraData: OptionalExtraHTMLData> HTMLDrawable
185    for Image<'a, ImageData, ExtraData>
186{
187    fn draw(self, canvas: &mut HTMLCanvas) {
188        let extra_data = match self.info.extra_data {
189            None => Default::default(),
190            Some(extra) => extra.as_ref().extra_html_data().unwrap_or_default(),
191        };
192        canvas.ensure_command_element(
193            self.info.id,
194            extra_data.tag.as_json_str(),
195            extra_data.extra_style.as_deref(),
196        );
197        let url = self.data.as_ref().url();
198        let bg = image_background_css(&self.background_color);
199        let bb = self.info.bounding_box;
200        canvas
201            .current_element_css_push(&format!(
202                "position: absolute; left: {}px; top: {}px; width: {}px; height: {}px",
203                bb.x, bb.y, bb.width, bb.height
204            ))
205            .current_element_css_push(&format!("background-color: {}", bg))
206            .current_element_css_push(&format!("background-image: url(\"{}\")", url))
207            .current_element_css_push("background-size: 100% 100%; background-repeat: no-repeat")
208            .current_element_css_push("background-position: center; display: block")
209            .current_element_css_push(&format!(
210                "border-radius: {}px {}px {}px {}px",
211                self.corner_radii.top_left,
212                self.corner_radii.top_right,
213                self.corner_radii.bottom_right,
214                self.corner_radii.bottom_left
215            ))
216            .current_element_css_push("pointer-events: auto");
217    }
218}