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