| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2026 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- use std::cmp::{max, min};
- use super::driver::ParleyDriverWrapper;
- use crate::{
- android::textinput::{AndroidTextInput, AndroidTextInputState},
- gfx::Point,
- mesh::Color,
- prop::{PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyStr},
- text,
- };
- macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor::android", $($arg)*); } }
- pub struct Editor {
- input: AndroidTextInput,
- pub state: AndroidTextInputState,
- pub recvr: async_channel::Receiver<AndroidTextInputState>,
- layout: parley::Layout<Color>,
- width: Option<f32>,
- text: PropertyStr,
- font_size: PropertyFloat32,
- text_color: PropertyColor,
- window_scale: PropertyFloat32,
- lineheight: PropertyFloat32,
- }
- impl Editor {
- pub fn new(
- text: PropertyStr,
- font_size: PropertyFloat32,
- text_color: PropertyColor,
- window_scale: PropertyFloat32,
- lineheight: PropertyFloat32,
- ) -> Self {
- let (sender, recvr) = async_channel::unbounded();
- let input = AndroidTextInput::new(sender);
- Self {
- input,
- state: Default::default(),
- recvr,
- layout: Default::default(),
- width: None,
- text,
- font_size,
- text_color,
- window_scale,
- lineheight,
- }
- }
- pub fn on_text_prop_changed(&mut self) {
- // Update GameTextInput state
- self.state.text = self.text.get();
- let text_len = self.state.text.len();
- self.state.select = (text_len, text_len);
- self.state.compose = None;
- self.input.set_state(self.state.clone());
- // Refresh our layout
- self.refresh();
- }
- pub fn on_buffer_changed(&mut self, atom: &mut PropertyAtomicGuard) {
- // Only refresh layout if text content actually changed
- // Avoid triggering expensive recomputes of layout and property tree.
- let old_text = self.text.get();
- if old_text == self.state.text {
- return
- }
- self.refresh();
- // Update the text attribute
- self.text.set(atom, &self.state.text);
- }
- pub fn focus(&mut self) {
- self.input.show();
- }
- pub fn unfocus(&mut self) {
- self.input.hide();
- }
- pub fn refresh(&mut self) {
- let font_size = self.font_size.get();
- let text_color = self.text_color.get();
- let window_scale = self.window_scale.get();
- let lineheight = self.lineheight.get();
- let mut underlines = vec![];
- if let Some((compose_start, compose_end)) = self.state.compose {
- underlines.push(compose_start..compose_end);
- }
- self.layout = text::make_layout(
- &self.state.text,
- text_color,
- font_size,
- lineheight,
- window_scale,
- self.width,
- &underlines,
- );
- }
- pub fn layout(&self) -> &parley::Layout<Color> {
- &self.layout
- }
- pub fn move_to_pos(&mut self, pos: Point) {
- let cursor = parley::Cursor::from_point(&self.layout, pos.x, pos.y);
- let cursor_idx = cursor.index();
- t!(" move_to_pos: {cursor_idx}");
- assert!(cursor_idx <= self.state.text.len());
- assert_eq!(self.state.text, self.text.get());
- self.state.select = (cursor_idx, cursor_idx);
- self.input.set_select(cursor_idx, cursor_idx);
- }
- pub fn select_word_at_point(&mut self, pos: Point) {
- let select = parley::Selection::word_from_point(&self.layout, pos.x, pos.y);
- assert!(!select.is_collapsed());
- let select = select.text_range();
- self.set_selection(select.start, select.end);
- }
- pub fn get_cursor_pos(&self) -> Point {
- let lineheight = self.lineheight.get();
- let cursor_idx = self.state.select.0;
- let cursor = if cursor_idx >= self.state.text.len() {
- parley::Cursor::from_byte_index(
- &self.layout,
- self.state.text.len(),
- parley::Affinity::Upstream,
- )
- } else {
- parley::Cursor::from_byte_index(&self.layout, cursor_idx, parley::Affinity::Downstream)
- };
- let cursor_rect = cursor.geometry(&self.layout, lineheight);
- Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32)
- }
- pub fn insert(&mut self, txt: &str, atom: &mut PropertyAtomicGuard) {
- // TODO: need to verify this is correct
- // Insert text by updating the state
- self.state.text.push_str(txt);
- let cursor_idx = self.state.text.len();
- self.state.select = (cursor_idx, cursor_idx);
- self.state.compose = None;
- self.input.set_state(self.state.clone());
- self.on_buffer_changed(atom);
- }
- pub fn driver(&mut self) -> ParleyDriverWrapper<'_> {
- ParleyDriverWrapper::new(&mut self.layout)
- }
- pub fn set_width(&mut self, w: f32) {
- self.width = Some(w);
- }
- pub fn width(&self) -> f32 {
- self.layout().full_width()
- }
- pub fn height(&self) -> f32 {
- self.layout().height()
- }
- pub fn selected_text(&self) -> Option<String> {
- let (start, end) = (self.state.select.0, self.state.select.1);
- if start == end {
- return None
- }
- let (start, end) = (min(start, end), max(start, end));
- Some(self.state.text[start..end].to_string())
- }
- pub fn selection(&self, side: isize) -> parley::Selection {
- assert!(side.abs() == 1);
- //t!("selection({side}) [state={:?}]", self.state);
- let (start, end) = (self.state.select.0, self.state.select.1);
- let (anchor, focus) = match side {
- -1 => (end, start),
- 1 => (start, end),
- _ => panic!(),
- };
- let anchor =
- parley::Cursor::from_byte_index(&self.layout, anchor, parley::Affinity::Downstream);
- let focus =
- parley::Cursor::from_byte_index(&self.layout, focus, parley::Affinity::Downstream);
- parley::Selection::new(anchor, focus)
- }
- pub fn set_selection(&mut self, select_start: usize, select_end: usize) {
- assert!(select_start <= self.state.text.len());
- assert!(select_end <= self.state.text.len());
- assert_eq!(self.state.text, self.text.get());
- self.state.select = (select_start, select_end);
- self.input.set_select(select_start, select_end);
- }
- pub fn select_all(&mut self) {
- let text_len = self.state.text.len();
- self.state.select = (0, text_len);
- self.input.set_select(0, text_len);
- }
- #[allow(dead_code)]
- pub fn buffer(&self) -> String {
- self.state.text.clone()
- }
- pub fn set_input_type(&mut self, input_type: u32) {
- self.input.set_input_type(input_type);
- }
- }
|