| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #![allow(dead_code, unused_imports, unused_variables)] |
| |
| use ftext::TextEditServerSessionProxyInterface; |
| |
| use { |
| anyhow::{format_err, Error}, |
| fidl_fuchsia_input_text::{ |
| self as ftext, CompositionUpdate as FidlCompositionUpdate, TextEditServer_Proxy, |
| TextFieldRequest, |
| }, |
| fidl_fuchsia_input_text_ext::IntoRange, |
| fuchsia_async as fasync, |
| futures::{channel::mpsc, lock::Mutex, prelude::*, FutureExt}, |
| std::{convert::TryInto, ops::Range, sync::Arc}, |
| text_edit_model::{CompositionUpdate, TextEditModel, TextFieldError}, |
| }; |
| |
| /// Manages interactions with the TextEditServer; owns the model, view model, and view. |
| /// |
| /// NOTE: The view model and view code has been removed pending a complete rewrite of the |
| /// integration with Carnelian. |
| #[derive(Debug, Clone)] |
| pub struct TextEditController { |
| inner: Arc<Mutex<Inner>>, |
| } |
| |
| #[derive(Debug)] |
| struct Inner { |
| id: String, |
| model: TextEditModel, |
| session: Option<ftext::TextEditServerSessionProxy>, |
| } |
| |
| impl TextEditController { |
| pub async fn new( |
| id: impl Into<String>, |
| field_options: ftext::TextFieldOptions, |
| ) -> Result<Self, Error> { |
| let model = TextEditModel::new(field_options)?; |
| let controller = TextEditController { |
| inner: Arc::new(Mutex::new(Inner { id: id.into(), model, session: None })), |
| }; |
| Ok(controller) |
| } |
| |
| async fn connect_to_text_edit_server( |
| self, |
| text_edit_server: TextEditServer_Proxy, |
| ) -> Result<(), Error> { |
| // The text field's client is the Text Edit Server. |
| // The host is the frontend app that displays a text box. |
| let (field_client_end, _field_host_end) = |
| fidl::endpoints::create_endpoints::<ftext::TextFieldMarker>()?; |
| let field_client_end = field_client_end.into_channel().into(); |
| |
| let (session_client_end, session_server_end) = |
| fidl::endpoints::create_endpoints::<ftext::TextEditServerSessionMarker>()?; |
| |
| let options = ftext::TextFieldOptions::EMPTY; |
| |
| let id = self.inner.lock().await.id.clone(); |
| log::info!("Registering text field '{}'", id); |
| let _ = text_edit_server |
| .register_focused_text_field(field_client_end, session_server_end, options) |
| .await?; |
| log::info!("Registered text field '{}'", id); |
| |
| self.inner.lock().await.session = Some(session_client_end.into_proxy()?); |
| let self_ = self.clone(); |
| |
| let field_host_stream = _field_host_end.into_stream()?; |
| let field_handler_future = field_host_stream |
| .try_for_each(move |req| { |
| log::info!("TextFieldRequest: {}", req.method_name()); |
| let self_ = self_.clone(); |
| |
| async move { |
| use TextFieldRequest::*; |
| |
| match req { |
| GetText { range, responder } => { |
| let mut result = |
| self_.clone().get_text(&range).await.map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| SetText { transaction_id, old_range, new_text, responder } => { |
| let mut result = self_ |
| .clone() |
| .set_text(&transaction_id, &old_range, new_text) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| SetSelection { transaction_id, selection, responder } => { |
| let mut result = self_ |
| .clone() |
| .set_selection(&transaction_id, selection) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| BeginTransaction { revision_id, responder } => { |
| let mut result = self_ |
| .clone() |
| .begin_transaction(&revision_id) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| CommitTransaction { transaction_id, responder } => { |
| let mut result = self_ |
| .clone() |
| .commit_transaction(&transaction_id) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| CommitTransactionInComposition { |
| transaction_id, |
| ctic_options, |
| responder, |
| } => { |
| let mut result = self_ |
| .clone() |
| .commit_transaction_in_composition( |
| transaction_id, |
| ctic_options.composition_update, |
| ) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| CancelTransaction { transaction_id, responder } => { |
| let mut result = self_ |
| .clone() |
| .cancel_transaction(&transaction_id) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| BeginComposition { revision_id, responder, .. } => { |
| let mut result = self_ |
| .clone() |
| .begin_composition(&revision_id) |
| .await |
| .map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| CompleteComposition { responder } => { |
| let mut result = |
| self_.clone().complete_composition().await.map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| CancelComposition { responder } => { |
| let mut result = |
| self_.clone().cancel_composition().await.map_err(Into::into); |
| responder.send(&mut result)?; |
| } |
| } |
| Ok(()) |
| } |
| }) |
| .map(|result| { |
| if let Err(e) = result { |
| log::error!("Controller loop failed with error: {:?}", e); |
| } |
| }); |
| fasync::Task::local(field_handler_future).detach(); |
| |
| // Notify the server of the field's initial state |
| self.notify_state_changed().await?; |
| |
| Ok(()) |
| } |
| |
| async fn notify_state_changed(self) -> Result<(), Error> { |
| let inner = self.inner.lock().await; |
| let state = inner.model.state(); |
| let session = inner.session.clone().expect("session exists"); |
| session.notify_state_changed(state).await?.to_anyhow_error()?; |
| Ok(()) |
| } |
| |
| async fn get_text(self, range: &ftext::Range) -> Result<String, TextFieldError> { |
| let range = range.into_range(); |
| self.inner.lock().await.model.get_text_range(range).map(|s| s.to_string()) |
| } |
| |
| async fn set_text( |
| self, |
| transaction_id: &ftext::TransactionId, |
| old_range: &ftext::Range, |
| new_text: String, |
| ) -> Result<(), TextFieldError> { |
| let old_range = old_range.into_range(); |
| self.inner.lock().await.model.set_text_range(transaction_id, old_range, new_text) |
| } |
| |
| async fn set_selection( |
| self, |
| transaction_id: &ftext::TransactionId, |
| selection: ftext::Selection, |
| ) -> Result<(), TextFieldError> { |
| self.inner.lock().await.model.set_selection(transaction_id, selection) |
| } |
| |
| async fn begin_transaction( |
| self, |
| revision_id: &ftext::RevisionId, |
| ) -> Result<ftext::TransactionId, TextFieldError> { |
| self.inner.lock().await.model.begin_transaction(revision_id) |
| } |
| |
| async fn commit_transaction( |
| self, |
| transaction_id: &ftext::TransactionId, |
| ) -> Result<ftext::TextFieldState, TextFieldError> { |
| let model = &mut self.inner.lock().await.model; |
| model.commit_transaction(transaction_id)?; |
| Ok(model.state()) |
| } |
| |
| async fn commit_transaction_in_composition( |
| self, |
| transaction_id: ftext::TransactionId, |
| composition_update: Option<FidlCompositionUpdate>, |
| ) -> Result<ftext::TextFieldState, TextFieldError> { |
| let composition_update = |
| composition_update.ok_or_else(|| TextFieldError::InvalidArgument)?.try_into()?; |
| let model = &mut self.inner.lock().await.model; |
| model.commit_transaction_in_composition(&transaction_id, composition_update)?; |
| Ok(model.state()) |
| } |
| |
| async fn cancel_transaction( |
| self, |
| transaction_id: &ftext::TransactionId, |
| ) -> Result<ftext::TextFieldState, TextFieldError> { |
| let model = &mut self.inner.lock().await.model; |
| model.cancel_transaction(transaction_id)?; |
| Ok(model.state()) |
| } |
| |
| async fn begin_composition( |
| self, |
| revision_id: &ftext::RevisionId, |
| ) -> Result<(), TextFieldError> { |
| self.inner.lock().await.model.begin_composition(revision_id) |
| } |
| |
| async fn complete_composition(&self) -> Result<ftext::TextFieldState, TextFieldError> { |
| let model = &mut self.inner.lock().await.model; |
| model.complete_composition()?; |
| Ok(model.state()) |
| } |
| |
| async fn cancel_composition(&self) -> Result<ftext::TextFieldState, TextFieldError> { |
| let model = &mut self.inner.lock().await.model; |
| model.cancel_composition()?; |
| Ok(model.state()) |
| } |
| } |
| |
| /// Converts any `Debug` error in a `Result` into `anyhow::Error`. |
| trait ToAnyhowError<T, F> |
| where |
| F: std::fmt::Debug, |
| { |
| fn to_anyhow_error(self) -> anyhow::Result<T>; |
| } |
| |
| impl<T, F> ToAnyhowError<T, F> for Result<T, F> |
| where |
| F: std::fmt::Debug, |
| { |
| fn to_anyhow_error(self) -> anyhow::Result<T> { |
| self.map_err(|e| format_err!("{:?}", e)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| anyhow::format_err, |
| fidl::endpoints::{create_proxy_and_stream, ClientEnd, ServerEnd}, |
| fidl_fuchsia_input_text_ext::IntoFuchsiaTextRange, |
| futures::prelude::*, |
| std::{collections::HashMap, ops::Deref}, |
| }; |
| |
| #[ignore = "currently broken. fxbug.dev/83711"] |
| #[fuchsia::test] |
| async fn smoke_test() { |
| let field_options = ftext::TextFieldOptions { |
| input_type: Some(ftext::InputType::Text), |
| ..ftext::TextFieldOptions::EMPTY |
| }; |
| let controller = TextEditController::new("text-edit", field_options) |
| .await |
| .expect("TextEditController::new"); |
| |
| let (server_proxy, server_request_stream) = |
| create_proxy_and_stream::<ftext::TextEditServer_Marker>() |
| .expect("create_proxy_and_stream"); |
| |
| let server = Server::new(); |
| let server_future = server.clone().run(server_request_stream); |
| fasync::Task::spawn(server_future).detach(); |
| let controller_future = controller |
| .clone() |
| .connect_to_text_edit_server(server_proxy.clone()) |
| .await |
| .expect("connect_to_text_edit_server"); |
| |
| let text_field_proxy = |
| server.get_field_snapshots().await.keys().next().expect("one text field proxy").clone(); |
| server |
| .send_edit_command(text_field_proxy.clone(), EditCommand::Type("abc".to_string())) |
| .await |
| .expect("send_edit_command"); |
| |
| let snapshot = server |
| .get_field_snapshots() |
| .await |
| .get(&text_field_proxy) |
| .expect("get snapshot") |
| .clone(); |
| assert_eq!("abc", snapshot.text.as_ref().unwrap()); |
| assert_eq!( |
| &ftext::Selection { base: 3, extent: 3, affinity: ftext::TextAffinity::Downstream }, |
| snapshot.state.as_ref().unwrap().selection.as_ref().unwrap() |
| ); |
| |
| server |
| .send_edit_command(text_field_proxy.clone(), EditCommand::MoveCaret(-1)) |
| .await |
| .expect("send_edit_command"); |
| server |
| .send_edit_command(text_field_proxy.clone(), EditCommand::Backspace) |
| .await |
| .expect("send_edit_command"); |
| |
| let snapshot = server |
| .get_field_snapshots() |
| .await |
| .get(&text_field_proxy) |
| .expect("get snapshot") |
| .clone(); |
| assert_eq!("ac", snapshot.text.as_ref().unwrap()); |
| assert_eq!( |
| &ftext::Selection { base: 1, extent: 1, affinity: ftext::TextAffinity::Downstream }, |
| snapshot.state.as_ref().unwrap().selection.as_ref().unwrap() |
| ); |
| } |
| |
| /// Needed because `TextField_Proxy` doesn't implement `Eq` and `Hash`. |
| #[derive(Debug, Clone)] |
| struct ProxyWrapper<P: fidl::endpoints::Proxy>(P); |
| |
| impl<P: fidl::endpoints::Proxy> AsRef<P> for ProxyWrapper<P> { |
| fn as_ref(&self) -> &P { |
| &self.0 |
| } |
| } |
| |
| impl<P: fidl::endpoints::Proxy> Deref for ProxyWrapper<P> { |
| type Target = P; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| impl<P: fidl::endpoints::Proxy> PartialEq for ProxyWrapper<P> { |
| fn eq(&self, other: &Self) -> bool { |
| self.0.as_channel().as_ref() == other.0.as_channel().as_ref() |
| } |
| } |
| |
| impl<P: fidl::endpoints::Proxy> Eq for ProxyWrapper<P> {} |
| |
| impl<P: fidl::endpoints::Proxy> std::hash::Hash for ProxyWrapper<P> { |
| fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
| self.0.as_channel().as_ref().hash(state); |
| } |
| } |
| |
| impl<P: fidl::endpoints::Proxy> From<P> for ProxyWrapper<P> { |
| fn from(proxy: P) -> Self { |
| ProxyWrapper(proxy) |
| } |
| } |
| |
| /// Rudimentary edit commands supported by this test server. |
| #[derive(Debug, Clone)] |
| enum EditCommand { |
| Type(String), |
| MoveCaret(i16), |
| Backspace, |
| } |
| |
| /// Rudimentary text server used for testing. |
| #[derive(Debug, Clone)] |
| struct Server { |
| fields: Arc<Mutex<HashMap<ProxyWrapper<ftext::TextFieldProxy>, FieldSnapshot>>>, |
| } |
| |
| #[derive(Debug, Clone)] |
| struct FieldSnapshot { |
| options: ftext::TextFieldOptions, |
| text: Option<String>, |
| state: Option<ftext::TextFieldState>, |
| } |
| |
| impl Server { |
| pub fn new() -> Self { |
| Server { fields: Arc::new(Mutex::new(HashMap::new())) } |
| } |
| |
| async fn run(self, stream: ftext::TextEditServer_RequestStream) { |
| use ftext::TextEditServer_Request::*; |
| let self_ = self.clone(); |
| let fut = stream |
| .try_for_each_concurrent(2, move |request| { |
| log::info!("TextEditServer_Request: {}", request.method_name()); |
| let self_ = self_.clone(); |
| async move { |
| match request { |
| RegisterFocusedTextField { |
| text_field, |
| server_session, |
| options, |
| responder, |
| } => { |
| let mut result = |
| self_.register_field(text_field, server_session, options).await; |
| responder.send(&mut result)?; |
| Ok(()) |
| } |
| } |
| } |
| }) |
| .await; |
| log::info!("Finished Server::run"); |
| } |
| |
| async fn register_field( |
| &self, |
| text_field: ClientEnd<ftext::TextFieldMarker>, |
| server_session: ServerEnd<ftext::TextEditServerSessionMarker>, |
| options: ftext::TextFieldOptions, |
| ) -> Result<(), ftext::TextEditServerError> { |
| // TODO: Add new error types to enum |
| let proxy = text_field.into_proxy().expect("into_proxy").into(); |
| let mut fields = self.fields.lock().await; |
| if fields.contains_key(&proxy) { |
| return Err(ftext::TextEditServerError::AlreadyRegistered); |
| } |
| |
| let self_ = self.clone(); |
| let proxy_ = proxy.clone(); |
| let session_future = server_session.into_stream().expect("into_stream").try_for_each( |
| move |req: ftext::TextEditServerSessionRequest| { |
| log::info!("{:#?}", &req); |
| let self_ = self_.clone(); |
| let proxy_ = proxy_.clone(); |
| async move { |
| match req { |
| ftext::TextEditServerSessionRequest::NotifyStateChanged { |
| state, |
| responder, |
| } => { |
| self_ |
| .update_field_snapshot(proxy_, state) |
| .await |
| .expect("update_field_snapshot"); |
| Ok(()) |
| } |
| _ => todo!(), |
| } |
| } |
| }, |
| ); |
| let session_task = fasync::Task::local(session_future); |
| |
| let snapshot = FieldSnapshot { options, text: None, state: None }; |
| fields.insert(proxy, snapshot); |
| |
| Ok(()) |
| } |
| |
| async fn update_field_snapshot( |
| &self, |
| text_field: ProxyWrapper<ftext::TextFieldProxy>, |
| state: ftext::TextFieldState, |
| ) -> Result<(), ftext::TextEditServerError> { |
| let text = text_field |
| .get_text(&mut state.contents_range.expect("contents_range").clone()) |
| .await |
| .expect("connecting to text field") |
| .expect("getting text"); |
| let mut fields = self.fields.lock().await; |
| let field_state = fields.get_mut(&text_field).expect("field is registered"); |
| field_state.text = Some(text); |
| field_state.state = Some(state); |
| Ok(()) |
| } |
| |
| pub async fn send_edit_command( |
| &self, |
| text_field: ProxyWrapper<ftext::TextFieldProxy>, |
| command: EditCommand, |
| ) -> Result<(), Error> { |
| let fields = self.fields.lock().await; |
| let state = |
| fields.get(&text_field).expect("registered field").state.as_ref().expect("state"); |
| let mut revision_id = |
| state.revision_id.as_ref().map(|x| x.clone()).expect("revision_id"); |
| let mut transaction_id = |
| text_field.begin_transaction(&mut revision_id).await?.to_anyhow_error()?; |
| let old_selection = state.selection.ok_or_else(|| format_err!("Missing selection"))?; |
| let old_range = old_selection.into_range(); |
| match command { |
| EditCommand::Type(s) => { |
| text_field |
| .set_text( |
| &mut transaction_id.clone(), |
| &mut old_range.into_fuchsia_text_range(), |
| s.as_str(), |
| ) |
| .await? |
| } |
| EditCommand::MoveCaret(i) => { |
| let new_position = if i < 0 { |
| old_selection.extent.checked_add(i as u32) |
| } else { |
| old_selection.extent.checked_sub((i * -1) as u32) |
| } |
| .ok_or_else(|| format_err!("Invalid caret position"))?; |
| let mut new_selection = ftext::Selection { |
| base: new_position, |
| extent: new_position, |
| affinity: ftext::TextAffinity::Downstream, |
| }; |
| text_field |
| .set_selection(&mut transaction_id.clone(), &mut new_selection) |
| .await? |
| } |
| EditCommand::Backspace => { |
| let mut range_to_delete = if old_range.is_empty() { |
| ftext::Range { start: old_selection.extent - 1, end: old_selection.extent } |
| } else { |
| old_range.into_fuchsia_text_range() |
| }; |
| text_field |
| .set_text(&mut transaction_id.clone(), &mut range_to_delete, "") |
| .await? |
| } |
| } |
| .to_anyhow_error()?; |
| |
| let new_state = |
| text_field.commit_transaction(&mut transaction_id).await?.to_anyhow_error()?; |
| self.clone().update_field_snapshot(text_field, new_state).await.to_anyhow_error()?; |
| Ok(()) |
| } |
| |
| pub async fn get_field_snapshots( |
| &self, |
| ) -> HashMap<ProxyWrapper<ftext::TextFieldProxy>, FieldSnapshot> { |
| self.fields.lock().await.clone() |
| } |
| } |
| } |