1use tilo_resource::text::splice_between;
4use tilo_resource::{Doc, Model, Path, PlainText, Resource, Segment, TextEdit};
5use ui_core::automerge::{ObjId, ReadDoc as _};
6use ui_core::widget::{
7 AppModel, Autocomplete, CodeEditor, Decorator, EventCtx, ModelPath, TextInput, Widget,
8};
9use ui_core::winit::event::WindowEvent;
10
11use super::dispatch::Dispatcher;
12
13pub trait EditsText: Widget {
16 fn show_text(&mut self, obj: ObjId);
19 fn hide_text(&mut self);
21 fn take_edited(&mut self) -> Option<String>;
23 fn reset_text(&mut self, text: &str);
25 fn shown_text(&self) -> &str;
26}
27
28impl EditsText for TextInput {
29 fn show_text(&mut self, obj: ObjId) {
30 self.bind_text(obj);
31 }
32 fn hide_text(&mut self) {
33 self.clear_text_binding();
34 }
35 fn take_edited(&mut self) -> Option<String> {
36 self.take_dirty()
37 }
38 fn reset_text(&mut self, text: &str) {
39 self.set_text(text);
40 }
41 fn shown_text(&self) -> &str {
42 self.value()
43 }
44}
45
46impl EditsText for CodeEditor {
47 fn show_text(&mut self, obj: ObjId) {
48 self.bind_text(obj);
49 }
50 fn hide_text(&mut self) {
51 self.clear_text_binding();
52 }
53 fn take_edited(&mut self) -> Option<String> {
54 self.take_dirty()
55 }
56 fn reset_text(&mut self, text: &str) {
57 self.set_text(text);
58 }
59 fn shown_text(&self) -> &str {
60 self.value()
61 }
62}
63
64impl EditsText for Autocomplete {
65 fn show_text(&mut self, obj: ObjId) {
66 self.input_mut().show_text(obj);
67 }
68 fn hide_text(&mut self) {
69 self.input_mut().hide_text();
70 }
71 fn take_edited(&mut self) -> Option<String> {
72 Autocomplete::take_edited(self)
73 }
74 fn reset_text(&mut self, text: &str) {
75 self.set_text(text);
76 }
77 fn shown_text(&self) -> &str {
78 self.text()
79 }
80}
81
82type Locate = Box<dyn Fn(&Doc) -> Option<ObjId>>;
84
85type Splice<R> = Box<
87 dyn Fn(
88 &Dispatcher<R>,
89 &mut ui_core::widget::Edits<'_>,
90 TextEdit,
91 ) -> Result<(), tilo_actions::ActionError>,
92>;
93
94pub struct TextControl<W: EditsText, R: Resource = PlainText> {
107 control: W,
108 dispatch: Option<Dispatcher<R>>,
109 locate: Locate,
110 splice: Splice<R>,
111 bound: Option<ObjId>,
113 stored: String,
115}
116
117impl<W: EditsText> TextControl<W> {
118 pub fn new(
120 control: W,
121 dispatcher: Dispatcher<PlainText>,
122 locate: impl Fn(&Doc) -> Option<ObjId> + 'static,
123 ) -> Self {
124 let mut text = Self::view_only(control, locate);
125 text.attach(dispatcher);
126 text
127 }
128
129 pub fn view_only(control: W, locate: impl Fn(&Doc) -> Option<ObjId> + 'static) -> Self {
131 Self {
132 control,
133 dispatch: None,
134 locate: Box::new(locate),
135 splice: Box::new(|dispatch, edits, edit| dispatch.apply_now(edits, &edit)),
136 bound: None,
137 stored: String::new(),
138 }
139 }
140}
141
142impl<W: EditsText, R: Resource<State = R> + Model> TextControl<W, R> {
143 pub fn at(control: W, dispatcher: Dispatcher<R>, path: Path<R, String>) -> Self {
147 let mut text = Self::view_only_at(control, path);
148 text.attach(dispatcher);
149 text
150 }
151
152 pub fn view_only_at(control: W, path: Path<R, String>) -> Self {
156 let segments = path.segments().to_vec();
157 Self {
158 control,
159 dispatch: None,
160 locate: Box::new(move |doc| text_at(doc, &segments)),
161 splice: Box::new(move |dispatch, edits, edit| {
162 let edit = match edit {
163 TextEdit::Splice { at, delete, insert } => path.splice(at, delete, insert),
164 TextEdit::ReplaceText { text } => path.replace(text),
165 };
166 dispatch.submit_now(edits, edit)
167 }),
168 bound: None,
169 stored: String::new(),
170 }
171 }
172}
173
174fn text_at(doc: &Doc, path: &[Segment]) -> Option<ObjId> {
177 let mut obj = ui_core::automerge::ROOT;
178 for segment in path {
179 let child = |obj: &ObjId, key: &str| match doc.get(obj, key) {
180 Ok(Some((ui_core::automerge::Value::Object(_), id))) => Some(id),
181 _ => None,
182 };
183 obj = match segment {
184 Segment::Field(key) | Segment::Key(key) => child(&obj, key)?,
185 Segment::Item(id) => {
186 let entry = child(&obj, id.as_str())?;
187 child(&entry, tilo_resource::shape::ENTRY_VALUE_KEY)?
188 }
189 Segment::Cell { .. } => return None,
190 };
191 }
192 Some(obj)
193}
194
195impl<W: EditsText, R: Resource> TextControl<W, R> {
196 pub fn attach(&mut self, dispatcher: Dispatcher<R>) {
198 self.dispatch = Some(dispatcher);
199 }
200
201 pub fn control(&self) -> &W {
202 &self.control
203 }
204
205 pub fn control_mut(&mut self) -> &mut W {
207 &mut self.control
208 }
209
210 pub fn pull(&mut self, doc: &Doc) {
213 self.follow(doc);
214 self.control.sync(doc, &ui_core::automerge::ROOT);
215 self.stored = self.control.shown_text().to_owned();
216 }
217
218 pub fn submit(&mut self, edits: &mut ui_core::widget::Edits<'_>) {
221 let Some(edited) = self.control.take_edited() else {
222 return;
223 };
224 let Some(edit) = splice_between(&self.stored, &edited) else {
225 return;
226 };
227 let applied = match &self.dispatch {
228 Some(dispatch) => (self.splice)(dispatch, edits, edit),
229 None => Err(tilo_actions::ActionError::Refused(
230 "this text is open to view only".into(),
231 )),
232 };
233 match applied {
234 Ok(()) => self.stored = edited,
235 Err(error) => {
236 tracing::warn!(%error, "text edit refused");
237 let stored = self.stored.clone();
238 self.control.reset_text(&stored);
239 }
240 }
241 }
242
243 fn follow(&mut self, doc: &Doc) {
244 let located = (self.locate)(doc);
245 if located == self.bound {
246 return;
247 }
248 if let Some(obj) = &located {
249 self.control.show_text(obj.clone());
250 } else {
251 self.control.hide_text();
252 self.control.reset_text("");
253 }
254 self.bound = located;
255 }
256}
257
258impl<W: EditsText + 'static, R: Resource> Decorator for TextControl<W, R> {
261 type Inner = W;
262
263 fn inner(&self) -> &W {
264 &self.control
265 }
266
267 fn inner_mut(&mut self) -> &mut W {
268 &mut self.control
269 }
270
271 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
272 {
273 let (doc, _) = ui_core::doc::document_ref(state, path);
274 self.follow(doc);
275 }
276 self.control.sync(state, path);
277 self.stored = self.control.shown_text().to_owned();
278 }
279
280 fn handle_event(&mut self, event: &WindowEvent, ctx: &mut EventCtx<'_>) {
281 self.control.handle_event(event, ctx);
282 self.submit(&mut ctx.edits());
283 }
284
285 fn perform_action(
286 &mut self,
287 action: &ui_core::a11y::Action,
288 key_path: &[u64],
289 state: &mut ui_core::widget::Edits<'_>,
290 path: &ModelPath,
291 ) -> bool {
292 let handled = self.control.perform_action(action, key_path, state, path);
293 self.submit(state);
294 handled
295 }
296
297 fn perform_semantic_action(
298 &mut self,
299 target: &str,
300 action: &ui_core::a11y::Action,
301 state: &mut ui_core::widget::Edits<'_>,
302 path: &ModelPath,
303 ) -> bool {
304 let handled = self
305 .control
306 .perform_semantic_action(target, action, state, path);
307 self.submit(state);
308 handled
309 }
310}