Skip to main content

graphix_package_gui/widgets/
text_editor.rs

1use super::{GuiW, GuiWidget, IcedElement, Message};
2use crate::types::{FontV, PaddingV};
3use anyhow::{Context, Result};
4use arcstr::ArcStr;
5use graphix_compiler::expr::ExprId;
6use graphix_rt::{Callable, CallableId, GXExt, GXHandle, Ref, TRef};
7use iced_widget::{self as widget, text_editor};
8use netidx::publisher::Value;
9use tokio::try_join;
10
11/// Multi-line text editor widget. Editable when on_edit callback is provided.
12pub(crate) struct TextEditorW<X: GXExt> {
13    gx: GXHandle<X>,
14    disabled: TRef<X, bool>,
15    content: text_editor::Content,
16    content_ref: TRef<X, String>,
17    on_edit: Ref<X>,
18    on_edit_callable: Option<Callable<X>>,
19    /// Last text we pushed via callback, used to suppress the echo
20    /// in handle_update so we don't destroy cursor/selection/undo state.
21    last_set_text: Option<String>,
22    placeholder: TRef<X, String>,
23    width: TRef<X, Option<f64>>,
24    height: TRef<X, Option<f64>>,
25    padding: TRef<X, PaddingV>,
26    font: TRef<X, Option<FontV>>,
27    size: TRef<X, Option<f64>>,
28}
29
30impl<X: GXExt> TextEditorW<X> {
31    pub(crate) async fn compile(gx: GXHandle<X>, source: Value) -> Result<GuiW<X>> {
32        let [(_, content), (_, disabled), (_, font), (_, height), (_, on_edit), (_, padding), (_, placeholder), (_, size), (_, width)] =
33            source.cast_to::<[(ArcStr, u64); 9]>().context("text_editor flds")?;
34        let (content, disabled, font, height, on_edit, padding, placeholder, size, width) =
35            try_join! {
36                gx.compile_ref(content),
37                gx.compile_ref(disabled),
38                gx.compile_ref(font),
39                gx.compile_ref(height),
40                gx.compile_ref(on_edit),
41                gx.compile_ref(padding),
42                gx.compile_ref(placeholder),
43                gx.compile_ref(size),
44                gx.compile_ref(width),
45            }?;
46        let on_edit_callable =
47            compile_callable!(gx, on_edit, "text_editor on_edit");
48        let content_tref: TRef<X, String> =
49            TRef::new(content).context("text_editor tref content")?;
50        let initial_text = content_tref.t.as_deref().unwrap_or("");
51        let editor_content = text_editor::Content::with_text(initial_text);
52        Ok(Box::new(Self {
53            gx: gx.clone(),
54            disabled: TRef::new(disabled).context("text_editor tref disabled")?,
55            content: editor_content,
56            content_ref: content_tref,
57            on_edit,
58            on_edit_callable,
59            last_set_text: None,
60            placeholder: TRef::new(placeholder)
61                .context("text_editor tref placeholder")?,
62            width: TRef::new(width).context("text_editor tref width")?,
63            height: TRef::new(height).context("text_editor tref height")?,
64            padding: TRef::new(padding).context("text_editor tref padding")?,
65            font: TRef::new(font).context("text_editor tref font")?,
66            size: TRef::new(size).context("text_editor tref size")?,
67        }))
68    }
69}
70
71impl<X: GXExt> GuiWidget<X> for TextEditorW<X> {
72    fn handle_update(
73        &mut self,
74        rt: &tokio::runtime::Handle,
75        id: ExprId,
76        v: &Value,
77    ) -> Result<bool> {
78        let mut changed = false;
79        changed |=
80            self.disabled.update(id, v).context("text_editor update disabled")?.is_some();
81        if let Some(new_text) =
82            self.content_ref.update(id, v).context("text_editor update content")?
83        {
84            // If this is the echo of text we just pushed, skip the
85            // destructive Content rebuild to preserve cursor/selection/undo.
86            if self.last_set_text.take().as_ref() != Some(new_text) {
87                self.content = text_editor::Content::with_text(new_text.as_str());
88                changed = true;
89            }
90        }
91        changed |= self
92            .placeholder
93            .update(id, v)
94            .context("text_editor update placeholder")?
95            .is_some();
96        changed |=
97            self.width.update(id, v).context("text_editor update width")?.is_some();
98        changed |=
99            self.height.update(id, v).context("text_editor update height")?.is_some();
100        changed |=
101            self.padding.update(id, v).context("text_editor update padding")?.is_some();
102        changed |= self.font.update(id, v).context("text_editor update font")?.is_some();
103        changed |= self.size.update(id, v).context("text_editor update size")?.is_some();
104        update_callable!(self, rt, id, v, on_edit, on_edit_callable, "text_editor on_edit recompile");
105        Ok(changed)
106    }
107
108    fn view(&self) -> IcedElement<'_> {
109        let mut te = widget::TextEditor::new(&self.content);
110        if !self.disabled.t.unwrap_or(false) && self.on_edit_callable.is_some() {
111            let content_id = self.content_ref.r.id;
112            te = te.on_action(move |a| Message::EditorAction(content_id, a));
113        }
114        let placeholder = self.placeholder.t.as_deref().unwrap_or("");
115        if !placeholder.is_empty() {
116            te = te.placeholder(placeholder);
117        }
118        if let Some(Some(w)) = self.width.t {
119            te = te.width(w as f32);
120        }
121        if let Some(Some(h)) = self.height.t {
122            te = te.height(h as f32);
123        }
124        if let Some(p) = self.padding.t.as_ref() {
125            te = te.padding(p.0);
126        }
127        if let Some(Some(f)) = self.font.t.as_ref() {
128            te = te.font(f.0);
129        }
130        if let Some(Some(sz)) = self.size.t {
131            te = te.size(sz as f32);
132        }
133        te.into()
134    }
135
136    fn editor_action(
137        &mut self,
138        id: ExprId,
139        action: &text_editor::Action,
140    ) -> Option<(CallableId, Value)> {
141        if id != self.content_ref.r.id {
142            return None;
143        }
144        self.content.perform(action.clone());
145        if action.is_edit() {
146            if let Some(callable) = &self.on_edit_callable {
147                let text = self.content.text();
148                self.last_set_text = Some(text.clone());
149                return Some((callable.id(), Value::String(text.into())));
150            }
151        }
152        None
153    }
154}