Skip to main content

teksilo_widgets/styles/
recipe_rich_text_editor_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `RichTextEditorStyle` impl.
5//!
6//! Frames the editor viewport in a TextInput-like border + padding +
7//! corner-radius surface. The `RichTextEditor` widget routes its
8//! body through this trait — apps installing a custom
9//! `RichTextEditorStyle` (per-call `.style(...)` or theme-wide via
10//! `style_slots.rich_text_editor`) swap the chrome wholesale
11//! without touching the editor's handlers, focus, or paint.
12
13use teksilo_core::build_context::BuildContext;
14use teksilo_core::color_prop::ColorProp;
15use teksilo_core::styles::{RichTextEditorStyle, RichTextEditorStyleConfig};
16use teksilo_core::widget_id::WidgetId;
17use teksilo_tokens::{BorderRole, CornerRadius, SurfaceRole};
18
19use crate::primitives::{Padding, RectWidget, ZStack};
20use crate::styles::recipe_text_input_style as field_dims;
21
22/// Default `RichTextEditorStyle` shipped with Teksilo. Wraps the
23/// viewport in a TextInput-like border + padding + corner-radius
24/// frame, with a focus-aware border. Returns the viewport id directly
25/// when read-only — viewers shouldn't carry an editable-field frame.
26#[derive(Debug, Default, Clone, Copy)]
27pub struct RecipeRichTextEditorStyle;
28
29impl RichTextEditorStyle for RecipeRichTextEditorStyle {
30    fn make_body(&self, cfg: &RichTextEditorStyleConfig, ctx: &mut BuildContext) -> WidgetId {
31        // Read-only viewers stay frameless — they're typically rendered
32        // inside an outer surface (Card, Panel) that owns the chrome.
33        // The widget-level `content_padding` knob is still honoured here:
34        // a user-set inset wraps the viewport in a Padding so the text
35        // gets the requested gutter against whatever surface the viewer
36        // is mounted in.
37        if cfg.is_read_only {
38            let content = match cfg.content_padding {
39                Some((t, r, b, l)) => ctx.add(Padding::new(t, r, b, l).child_id(cfg.viewport)),
40                None => cfg.viewport,
41            };
42            return match cfg.background.clone() {
43                Some(bg) => {
44                    let bg_id = ctx.add(RectWidget::new().background(bg));
45                    ctx.add(ZStack::new().add_child(bg_id).add_child(content))
46                }
47                None => content,
48            };
49        }
50
51        // Editable: TextInput-style focus-aware frame. The widget-level
52        // `content_padding` replaces the default field insets when set.
53        let theme = ctx.theme_signal().get();
54        let focus_ring_width = theme.shape.focus_ring_width;
55        let field_border_width = field_dims::TEXT_FIELD_BORDER_WIDTH;
56        let border_role = cfg.is_focused.map(|f| {
57            if *f {
58                BorderRole::Focused
59            } else {
60                BorderRole::Default
61            }
62        });
63        let border_width_signal = cfg.is_focused.map(move |f| {
64            if *f {
65                focus_ring_width
66            } else {
67                field_border_width
68            }
69        });
70        let bg = RectWidget::new()
71            .background(
72                cfg.background
73                    .clone()
74                    .unwrap_or_else(|| SurfaceRole::Content.into()),
75            )
76            .border_color(ColorProp::DynamicBorderRole(border_role))
77            .border_width(border_width_signal)
78            .corner_radius(CornerRadius::uniform(field_dims::TEXT_FIELD_CORNER_RADIUS));
79        let bg_id = ctx.add(bg);
80        let (pt, pr, pb, pl) = cfg.content_padding.unwrap_or((
81            field_dims::TEXT_FIELD_PADDING_VERTICAL,
82            field_dims::TEXT_FIELD_PADDING_HORIZONTAL,
83            field_dims::TEXT_FIELD_PADDING_VERTICAL,
84            field_dims::TEXT_FIELD_PADDING_HORIZONTAL,
85        ));
86        let padded = ctx.add(Padding::new(pt, pr, pb, pl).child_id(cfg.viewport));
87        ctx.add(ZStack::new().add_child(bg_id).add_child(padded))
88    }
89}