use anyhow::Result;
use femtovg::{Color, FontId, Paint, Path};
use relm4::gtk::glib::GString;
use relm4::gtk::prelude::IMContextExt;
use relm4::gtk::{
TextBuffer,
gdk::{Key, ModifierType, Rectangle},
};
use std::{borrow::Cow, ops::Range};
use relm4::gtk::prelude::*;
use crate::{
configuration::APP_CONFIG,
femtovg_area,
ime::preedit::{Preedit, UnderlineKind},
math::Vec2D,
sketch_board::{KeyEventMsg, MouseButton, MouseEventMsg, MouseEventType, TextEventMsg},
style::Style,
};
use super::{Drawable, DrawableClone, InputContext, Tool, ToolUpdateResult, Tools};
use crate::sketch_board::SketchBoardInput;
use relm4::Sender;
use relm4::gtk::gdk::DisplayManager;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Copy)]
struct Decoration {
color: crate::style::Color,
kind: DecorationKind,
}
#[derive(Clone, Copy)]
enum DecorationKind {
Outline { width: f32 },
Shadow { offset: Vec2D },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum TextEffect {
#[default]
None,
Inverted,
Contrast,
Shadow,
}
impl TextEffect {
fn next(self) -> Self {
match self {
Self::None => Self::Inverted,
Self::Inverted => Self::Contrast,
Self::Contrast => Self::Shadow,
Self::Shadow => Self::None,
}
}
fn decoration(self, text_color: crate::style::Color, line_width: f32) -> Option<Decoration> {
match self {
Self::None => None,
Self::Inverted => Some(Decoration {
color: text_color.inverted(),
kind: DecorationKind::Outline {
width: line_width * 2.0,
},
}),
Self::Contrast => Some(Decoration {
color: text_color.contrast(),
kind: DecorationKind::Outline {
width: line_width * 2.0,
},
}),
Self::Shadow => Some(Decoration {
color: text_color.contrast().with_alpha(160),
kind: DecorationKind::Shadow {
offset: Vec2D::new(line_width * 1.5, line_width * 1.5),
},
}),
}
}
}
#[derive(Clone, Debug)]
pub struct Text {
pos: Vec2D,
editing: bool,
text_buffer: TextBuffer,
style: Style,
preedit: Option<Preedit>,
im_context: Option<InputContext>,
rect: RefCell<Rectangle>,
glyphs: RefCell<Vec<Vec<Rectangle>>>,
line_ranges: RefCell<Vec<Range<usize>>>,
cursor_visible: RefCell<bool>,
draw_rect: RefCell<bool>,
effect: TextEffect,
font_ids: Vec<FontId>,
}
struct DisplayContent<'a> {
text: Cow<'a, str>,
cursor_byte_pos: usize,
preedit_range: Option<Range<usize>>,
}
struct LineLayout {
range: Range<usize>,
baseline: f32,
}
struct TextDrawingContext<'a> {
paint: &'a Paint,
text: &'a str,
lines: &'a [LineLayout],
}
#[derive(Clone, Copy)]
struct CursorMetrics {
top_offset: f32,
line_height: f32,
}
impl Text {
fn new(pos: Vec2D, style: Style, im_context: Option<InputContext>) -> Self {
let text_buffer = TextBuffer::new(None);
text_buffer.set_enable_undo(true);
Self {
pos,
text_buffer,
editing: true,
style,
preedit: None,
im_context,
rect: RefCell::new(Rectangle::new(0, 0, 0, 0)),
glyphs: RefCell::new(Vec::new()),
line_ranges: RefCell::new(Vec::new()),
cursor_visible: RefCell::new(true),
draw_rect: RefCell::new(true),
effect: TextEffect::default(),
font_ids: femtovg_area::font_stack().to_vec(),
}
}
fn byte_index_from_char_index(text: &str, char_index: usize) -> usize {
text.char_indices()
.nth(char_index)
.map(|(idx, _)| idx)
.unwrap_or_else(|| text.len())
}
fn char_index_from_byte_index(text: &str, byte_index: usize) -> usize {
let mut idx = byte_index.min(text.len());
while idx > 0 && !text.is_char_boundary(idx) {
idx -= 1;
}
text[..idx].chars().count()
}
fn display_text<'a>(&self, base_text: &'a str) -> DisplayContent<'a> {
let cursor_char_index = self.text_buffer.cursor_position() as usize;
let base_cursor_byte = Self::byte_index_from_char_index(base_text, cursor_char_index);
if self.editing {
if let Some(preedit) = &self.preedit {
if preedit.text.is_empty() {
return DisplayContent {
text: Cow::Borrowed(base_text),
cursor_byte_pos: base_cursor_byte,
preedit_range: None,
};
}
let mut composed = String::with_capacity(base_text.len() + preedit.text.len());
composed.push_str(&base_text[..base_cursor_byte]);
composed.push_str(&preedit.text);
composed.push_str(&base_text[base_cursor_byte..]);
let preedit_char_len = preedit.text.chars().count();
let cursor_chars = preedit
.cursor_chars
.map(|value| value.min(preedit_char_len))
.unwrap_or(preedit_char_len);
let preedit_cursor_byte =
Self::byte_index_from_char_index(&preedit.text, cursor_chars);
let composed_cursor_byte = base_cursor_byte + preedit_cursor_byte;
DisplayContent {
text: Cow::Owned(composed),
cursor_byte_pos: composed_cursor_byte,
preedit_range: Some(base_cursor_byte..base_cursor_byte + preedit.text.len()),
}
} else {
DisplayContent {
text: Cow::Borrowed(base_text),
cursor_byte_pos: base_cursor_byte,
preedit_range: None,
}
}
} else {
DisplayContent {
text: Cow::Borrowed(base_text),
cursor_byte_pos: base_cursor_byte,
preedit_range: None,
}
}
}
fn get_text(&self) -> GString {
self.text_buffer.text(
&self.text_buffer.start_iter(),
&self.text_buffer.end_iter(),
false,
)
}
}
impl Drawable for Text {
fn draw(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
font: FontId,
_bounds: (Vec2D, Vec2D),
) -> Result<()> {
let gtext = self.get_text();
let base_text = gtext.as_str();
let display = self.display_text(base_text);
let text = display.text.as_ref();
let mut base_paint: Paint = self.style.into();
base_paint.set_font(&[font]);
if self.font_ids.is_empty() {
base_paint.set_font(&[font]);
} else {
base_paint.set_font(&self.font_ids);
}
let transform = canvas.transform();
let canva_scale = transform.average_scale();
let width = _bounds.1.x - self.pos.x;
let lines = canvas.break_text_vec(width, text, &base_paint)?;
self.line_ranges.replace(lines.clone());
let font_metrics = canvas.measure_font(&base_paint)?;
let measured_cursor = canvas
.measure_text(self.pos.x, self.pos.y, "|", &base_paint)
.ok();
let mut line_height = measured_cursor
.as_ref()
.map(|metrics| metrics.height())
.unwrap_or(0.0);
if line_height <= 0.0 {
let ascender_plus_descender = font_metrics.ascender() + font_metrics.descender();
if ascender_plus_descender.abs() > f32::EPSILON {
line_height = ascender_plus_descender.abs() / canva_scale;
}
}
if line_height <= 0.0 {
line_height = font_metrics.height() / canva_scale;
}
let cursor_top_offset = -line_height;
line_height *= 1.2;
let mut line_layouts: Vec<LineLayout> = Vec::with_capacity(lines.len());
let mut baseline = self.pos.y;
for line_range in &lines {
line_layouts.push(LineLayout {
range: line_range.clone(),
baseline,
});
baseline += line_height;
}
let cursor_metrics = CursorMetrics {
top_offset: cursor_top_offset,
line_height,
};
let layout_context = TextDrawingContext {
paint: &base_paint,
text,
lines: &line_layouts,
};
let mut cursor_visible = self.cursor_visible.borrow_mut();
if let Some((sel_start_iter, sel_end_iter)) = self.text_buffer.selection_bounds() {
let sel_start = sel_start_iter.offset() as usize;
let sel_end = sel_end_iter.offset() as usize;
for line in &line_layouts {
let start_index = text[..line.range.start].chars().count();
let end_index = text[..line.range.end].chars().count();
let overlap_start = sel_start.max(start_index);
let overlap_end = sel_end.min(end_index);
if overlap_start >= overlap_end {
continue;
}
let segments = self.segments_for_line_span(
canvas,
&layout_context,
line,
overlap_start..overlap_end,
);
for (start_x, end_x) in segments {
let mut path = Path::new();
let offset_y = cursor_metrics.line_height * 0.1;
let y = line.baseline + cursor_metrics.top_offset + offset_y;
let h = cursor_metrics.line_height;
let x = start_x;
let w = end_x - start_x;
path.rect(x, y, w, h);
let mut paint = Paint::color(Color::rgbaf(0.3, 0.5, 1.0, 0.3)); paint.set_anti_alias(true);
canvas.fill_path(&path, &paint);
}
}
*cursor_visible = false;
} else {
*cursor_visible = true;
}
let mut draw_baseline = self.pos.y;
let mut rect = self.rect.borrow_mut();
let mut glyphs = self.glyphs.borrow_mut();
let mut tl = Vec2D::zero();
let mut wh = Vec2D::zero();
glyphs.clear();
{
let mut first_segment = true;
for line in &line_layouts {
let mut line_glyphs = Vec::new();
let start = text[..line.range.start].chars().count();
let end = text[..line.range.end].chars().count();
for i in start..end {
let segments =
self.segments_for_line_span(canvas, &layout_context, line, i..i + 1);
for (start_x, end_x) in segments {
let offset_y = cursor_metrics.line_height * 0.1;
let y = line.baseline + cursor_metrics.top_offset + offset_y;
let h = cursor_metrics.line_height;
let x = start_x;
let w = end_x - start_x;
line_glyphs.push(Rectangle::new(x as i32, y as i32, w as i32, h as i32));
if first_segment {
tl.y = y - 1.0;
tl.x = x - 1.0;
first_segment = false;
}
wh.x = (end_x - tl.x).max(wh.x);
wh.y = y + h - tl.y + 1.0;
}
}
glyphs.push(line_glyphs);
}
wh.x += 1.0; rect.set_height(wh.y as i32);
rect.set_width(wh.x as i32);
rect.set_x(tl.x as i32);
rect.set_y(tl.y as i32);
}
if *self.draw_rect.borrow() {
let mut rect_paint = Path::new();
rect_paint.move_to(self.pos.x, self.pos.y);
rect_paint.rect(tl.x, tl.y, wh.x, wh.y);
let mut paint = Paint::color(Color::rgbaf(1.0, 0.5, 0.3, 0.3)); paint.set_anti_alias(true);
paint.set_line_width(2.0);
canvas.stroke_path(&rect_paint, &paint);
}
let decoration_paint = self
.effect
.decoration(self.style.color, base_paint.line_width())
.map(|dec| {
let mut paint = base_paint.clone();
paint.set_color(dec.color.into());
if let DecorationKind::Outline { width } = dec.kind {
paint.set_line_width(width);
}
(dec.kind, paint)
});
for line_range in &lines {
let line = &text[line_range.clone()];
if let Some((kind, paint)) = &decoration_paint {
match kind {
DecorationKind::Shadow { offset } => {
canvas.fill_text(
self.pos.x + offset.x,
draw_baseline + offset.y,
line,
paint,
)?;
}
DecorationKind::Outline { .. } => {
canvas.stroke_text(self.pos.x, draw_baseline, line, paint)?;
}
}
}
canvas.fill_text(self.pos.x, draw_baseline, line, &base_paint)?;
draw_baseline += line_height;
}
if self.editing
&& let (Some(preedit), Some(preedit_range)) = (&self.preedit, &display.preedit_range)
{
self.draw_preedit_overlays(
canvas,
font,
&layout_context,
preedit,
preedit_range,
cursor_metrics,
)?;
}
if self.editing {
self.draw_cursor_and_update_ime(
canvas,
font,
&layout_context,
cursor_metrics,
display.cursor_byte_pos,
*cursor_visible,
);
}
Ok(())
}
}
impl Text {
fn draw_preedit_background(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
context: &TextDrawingContext<'_>,
preedit: &Preedit,
preedit_range: &Range<usize>,
cursor: CursorMetrics,
) {
for span in &preedit.spans {
let Some(background_color) = span.background else {
continue;
};
let global_start = preedit_range.start + span.range.start;
let global_end = preedit_range.start + span.range.end;
for line in context.lines {
let overlap_start = global_start.max(line.range.start);
let overlap_end = global_end.min(line.range.end);
if overlap_start >= overlap_end {
continue;
}
let overlap_start_char =
Self::char_index_from_byte_index(context.text, overlap_start);
let overlap_end_char = Self::char_index_from_byte_index(context.text, overlap_end);
let segments = self.segments_for_line_span(
canvas,
context,
line,
overlap_start_char..overlap_end_char,
);
for (start_x, end_x) in segments {
let width = (end_x - start_x).max(0.0);
if width <= f32::EPSILON {
continue;
}
let mut path = Path::new();
let offset_y = cursor.line_height * 0.1;
let top = line.baseline + cursor.top_offset + offset_y;
path.rect(start_x, top, width, cursor.line_height);
let mut fill_paint = Paint::color(background_color.into());
fill_paint.set_anti_alias(true);
canvas.fill_path(&path, &fill_paint);
}
}
}
}
fn draw_preedit_overlays(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
font: FontId,
context: &TextDrawingContext<'_>,
preedit: &Preedit,
preedit_range: &Range<usize>,
cursor: CursorMetrics,
) -> Result<()> {
self.draw_preedit_background(canvas, context, preedit, preedit_range, cursor);
for span in &preedit.spans {
let global_start = preedit_range.start + span.range.start;
let global_end = preedit_range.start + span.range.end;
for line in context.lines {
let overlap_start = global_start.max(line.range.start);
let overlap_end = global_end.min(line.range.end);
if overlap_start >= overlap_end {
continue;
}
let overlap_start_char =
Self::char_index_from_byte_index(context.text, overlap_start);
let overlap_end_char = Self::char_index_from_byte_index(context.text, overlap_end);
let segments = self.segments_for_line_span(
canvas,
context,
line,
overlap_start_char..overlap_end_char,
);
if segments.is_empty() {
continue;
}
if let Some(color) = span.foreground {
let mut overlay_paint: Paint = self.style.into();
overlay_paint.set_font(&[font]);
overlay_paint.set_color(color.into());
for (start_x, end_x) in &segments {
let width = (*end_x - *start_x).max(0.0);
if width <= f32::EPSILON {
continue;
}
canvas.save();
canvas.scissor(
*start_x - 1.0,
line.baseline + cursor.top_offset - 1.0,
width + 2.0,
cursor.line_height + 4.0,
);
canvas.fill_text(
self.pos.x,
line.baseline,
&context.text[line.range.clone()],
&overlay_paint,
)?;
canvas.restore();
}
}
if span.underline != UnderlineKind::None {
let color = span
.underline_color
.or(span.foreground)
.unwrap_or(self.style.color);
self.draw_underline_segments(
canvas,
&segments,
line.baseline + cursor.top_offset,
cursor.line_height,
span.underline,
color,
);
}
}
}
Ok(())
}
fn draw_underline_segments(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
segments: &[(f32, f32)],
line_top: f32,
cursor_height: f32,
underline: UnderlineKind,
color: crate::style::Color,
) {
if segments.is_empty() {
return;
}
let mut paint = Paint::color(color.into());
let thickness = (cursor_height * 0.02).clamp(1.0, cursor_height / 2.0);
paint.set_line_width(thickness);
paint.set_anti_alias(true);
let base_y = line_top + cursor_height + 2.0;
for &(start_x, end_x) in segments {
if end_x - start_x <= f32::EPSILON {
continue;
}
match underline {
UnderlineKind::Double => {
let mut first = Path::new();
first.move_to(start_x, base_y - thickness);
first.line_to(end_x, base_y - thickness);
canvas.stroke_path(&first, &paint);
let mut second = Path::new();
second.move_to(start_x, base_y + thickness * 0.5);
second.line_to(end_x, base_y + thickness * 0.5);
canvas.stroke_path(&second, &paint);
}
UnderlineKind::None => {}
_ => {
let mut path = Path::new();
path.move_to(start_x, base_y);
path.line_to(end_x, base_y);
canvas.stroke_path(&path, &paint);
}
}
}
}
fn segments_for_line_span(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
context: &TextDrawingContext<'_>,
line: &LineLayout,
range: Range<usize>,
) -> Vec<(f32, f32)> {
if range.start >= range.end {
return Vec::new();
}
let chars_without_newline: Vec<(usize, char)> = context.text.char_indices().collect();
let range_start_byte = chars_without_newline
.get(range.start)
.map(|(i, _)| *i)
.unwrap_or(context.text.len());
let range_end_byte = chars_without_newline
.get(range.end)
.map(|(i, _)| *i)
.unwrap_or(context.text.len());
let line_start = line.range.start;
let line_end = line.range.end;
let overlap_start = range_start_byte.max(line_start).min(line_end);
let overlap_end = range_end_byte.max(line_start).min(line_end);
if overlap_start >= overlap_end {
return Vec::new();
}
let line_text = &context.text[line.range.clone()];
let start_byte = overlap_start.saturating_sub(line_start);
let end_byte = overlap_end.saturating_sub(line_start);
let prefix = &line_text[..start_byte];
let selected = &line_text[start_byte..end_byte].replace("\n", "");
let start_x: f32 = self.pos.x + Self::text_width(canvas, context.paint, prefix);
let width = Self::text_width(canvas, context.paint, selected);
vec![(start_x, start_x + width.max(0.0))]
}
fn caret_top_left(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
context: &TextDrawingContext<'_>,
cursor_byte_pos: usize,
cursor: CursorMetrics,
) -> (f32, f32) {
if context.lines.is_empty() {
return (self.pos.x, self.pos.y + cursor.top_offset);
}
let mut newline_pending_baseline: Option<f32> = None;
for line in context.lines {
let line_text = &context.text[line.range.clone()];
if cursor_byte_pos < line.range.end {
let prefix_len = cursor_byte_pos
.saturating_sub(line.range.start)
.min(line_text.len());
let prefix = &line_text[..prefix_len];
let offset = Self::text_width(canvas, context.paint, prefix);
return (self.pos.x + offset, line.baseline + cursor.top_offset);
}
if cursor_byte_pos == line.range.end {
if line_text.ends_with('\n') {
newline_pending_baseline =
Some(line.baseline + cursor.top_offset + cursor.line_height);
continue;
}
let offset = Self::text_width(canvas, context.paint, line_text);
return (self.pos.x + offset, line.baseline + cursor.top_offset);
}
}
if let Some(baseline) = newline_pending_baseline {
return (self.pos.x, baseline);
}
if let Some(last_line) = context.lines.last() {
let line_text = &context.text[last_line.range.clone()];
let offset = Self::text_width(canvas, context.paint, line_text);
(
self.pos.x + offset,
last_line.baseline + cursor.top_offset + cursor.line_height,
)
} else {
(self.pos.x, self.pos.y + cursor.top_offset)
}
}
fn draw_cursor_and_update_ime(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
font: FontId,
context: &TextDrawingContext<'_>,
cursor: CursorMetrics,
cursor_byte_pos: usize,
cursor_visible: bool,
) {
let (cursor_x, cursor_top) = self.caret_top_left(canvas, context, cursor_byte_pos, cursor);
let caret_height = cursor.line_height;
let mut caret_paint: Paint = self.style.into();
caret_paint.set_font(&[font]);
let extra_height = caret_height * 0.1;
if cursor_visible {
let mut path = Path::new();
path.move_to(cursor_x, cursor_top + extra_height);
path.line_to(cursor_x, cursor_top + caret_height + extra_height);
canvas.fill_path(&path, &caret_paint);
}
if self.editing
&& let Some(handle) = &self.im_context
{
let transform = canvas.transform();
let widget_scale = handle.widget.scale_factor().max(1) as f32;
let (x1, y1) = transform.transform_point(cursor_x, cursor_top + extra_height);
let (x2, y2) = transform.transform_point(
cursor_x + 1.0,
cursor_top + caret_height + extra_height + 1.0,
);
let logical_x = (x1 / widget_scale).floor() as i32;
let logical_y = (y1 / widget_scale).floor() as i32;
let logical_width = ((x2 - x1).abs() / widget_scale).ceil().max(1.0) as i32;
let logical_height = ((y2 - y1).abs() / widget_scale).ceil().max(1.0) as i32;
let rect = Rectangle::new(logical_x, logical_y, logical_width, logical_height.max(1));
handle.im_context.set_cursor_location(&rect);
}
}
fn text_width(
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
paint: &Paint,
text: &str,
) -> f32 {
if text.is_empty() {
return 0.0;
}
canvas
.measure_text(0.0, 0.0, text, paint)
.map(|metrics| metrics.width())
.unwrap_or(0.0)
}
}
#[derive(Default)]
pub struct TextTool {
text: Option<Text>,
style: Style,
input_enabled: bool,
im_context: Option<InputContext>,
sender: Option<Sender<SketchBoardInput>>,
drag_start_pos: Vec2D,
dragged: Rc<RefCell<bool>>,
alt_tap: bool,
}
impl Tool for TextTool {
fn get_tool_type(&self) -> super::Tools {
Tools::Text
}
fn input_enabled(&self) -> bool {
self.input_enabled
}
fn set_input_enabled(&mut self, value: bool) {
self.input_enabled = value;
}
fn set_im_context(&mut self, context: Option<InputContext>) {
self.im_context = context.clone();
if let Some(text) = &mut self.text {
text.im_context = context;
}
}
fn get_drawable(&self) -> Option<&dyn Drawable> {
match &self.text {
Some(d) => Some(d),
None => None,
}
}
fn handle_style_event(&mut self, style: Style) -> ToolUpdateResult {
self.style = style;
if let Some(t) = &mut self.text {
t.style = style;
ToolUpdateResult::Redraw
} else {
ToolUpdateResult::Unmodified
}
}
fn handle_text_event(&mut self, event: crate::sketch_board::TextEventMsg) -> ToolUpdateResult {
if let Some(t) = &mut self.text {
match event {
TextEventMsg::Commit(text) => {
Self::handle_text_buffer_action(t, Action::Delete, ActionScope::None);
t.preedit = None;
t.text_buffer.insert_at_cursor(&text);
ToolUpdateResult::Redraw
}
TextEventMsg::Preedit {
text,
cursor_chars,
spans,
} => {
if text.is_empty() {
if t.preedit.take().is_some() {
ToolUpdateResult::Redraw
} else {
ToolUpdateResult::Unmodified
}
} else {
t.preedit = Some(Preedit {
text,
cursor_chars,
spans,
});
ToolUpdateResult::Redraw
}
}
TextEventMsg::PreeditEnd => {
if t.preedit.take().is_some() {
ToolUpdateResult::Redraw
} else {
ToolUpdateResult::Unmodified
}
}
}
} else {
ToolUpdateResult::Unmodified
}
}
fn handle_key_event(&mut self, event: KeyEventMsg) -> ToolUpdateResult {
if !matches!(event.key, Key::Alt_L | Key::Alt_R) {
self.alt_tap = false;
}
let mut tool_update_result = ToolUpdateResult::StopPropagation;
if let Some(t) = &mut self.text {
match event.key {
Key::Return => match event.modifier {
ModifierType::SHIFT_MASK => {
Self::handle_text_buffer_action(t, Action::Delete, ActionScope::None);
t.text_buffer.insert_at_cursor("\n");
tool_update_result = ToolUpdateResult::RedrawAndStopPropagation;
}
_ => {
let content = t.get_text();
if content.is_empty() {
self.text = None;
self.input_enabled = false;
tool_update_result = ToolUpdateResult::RedrawAndStopPropagation;
} else {
t.preedit = None;
t.editing = false;
t.im_context = None;
t.text_buffer.select_range(
&t.text_buffer.start_iter(),
&t.text_buffer.start_iter(),
);
*t.draw_rect.borrow_mut() = false;
let result = t.clone_box();
self.text = None;
self.input_enabled = false;
tool_update_result = ToolUpdateResult::Commit(result);
}
}
},
Key::Escape => {
tool_update_result = self.handle_dismissed();
}
Key::Alt_L | Key::Alt_R => {
self.alt_tap = true;
}
Key::BackSpace | Key::Delete => {
let ctrl_mask = match event.key {
Key::BackSpace => ActionScope::BackwardWord,
Key::Delete => ActionScope::ForwardWord,
_ => ActionScope::None,
};
let other_mask = match event.key {
Key::BackSpace => ActionScope::BackwardChar,
Key::Delete => ActionScope::ForwardChar,
_ => ActionScope::None,
};
if event.modifier == ModifierType::CONTROL_MASK {
tool_update_result =
Self::handle_text_buffer_action(t, Action::Delete, ctrl_mask);
} else {
tool_update_result =
Self::handle_text_buffer_action(t, Action::Delete, other_mask);
}
}
Key::Left | Key::Right | Key::Up | Key::Down => {
let ctrl_mask = match event.key {
Key::Left => ActionScope::BackwardWord,
Key::Right => ActionScope::ForwardWord,
Key::Up => ActionScope::BackwardLineAndWord,
Key::Down => ActionScope::ForwardLineAndWord,
_ => ActionScope::None,
};
let other_mask = match event.key {
Key::Left => ActionScope::BackwardChar,
Key::Right => ActionScope::ForwardChar,
Key::Up => ActionScope::BackwardLineAndWord,
Key::Down => ActionScope::ForwardLineAndWord,
_ => ActionScope::None,
};
let combine_mask = match event.key {
Key::Left => ActionScope::BackwardWord,
Key::Right => ActionScope::ForwardWord,
Key::Up => ActionScope::BackwardLineAndWord,
Key::Down => ActionScope::ForwardLineAndWord,
_ => ActionScope::None,
};
let ctrl_alt_mask = match event.key {
Key::Left => ActionScope::Left,
Key::Right => ActionScope::Right,
Key::Up => ActionScope::Up,
Key::Down => ActionScope::Down,
_ => ActionScope::None,
};
match event.modifier {
ModifierType::ALT_MASK => {
tool_update_result = ToolUpdateResult::Unmodified;
}
ModifierType::CONTROL_MASK => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::MoveCursor, ctrl_mask);
}
ModifierType::SHIFT_MASK => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::Select, other_mask);
}
m if m == ModifierType::ALT_MASK | ModifierType::CONTROL_MASK => {
tool_update_result = Self::handle_text_buffer_action(
t,
Action::MoveOrigin,
ctrl_alt_mask,
);
}
m if m
== ModifierType::ALT_MASK
| ModifierType::CONTROL_MASK
| ModifierType::SHIFT_MASK =>
{
tool_update_result = Self::handle_text_buffer_action(
t,
Action::NudgeOrigin,
ctrl_alt_mask,
);
}
m if m == ModifierType::CONTROL_MASK | ModifierType::SHIFT_MASK => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::Select, combine_mask);
}
_ => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::MoveCursor, other_mask);
}
}
}
Key::Home | Key::End => {
let ctrl_mask = match event.key {
Key::Home => ActionScope::BufferStart,
Key::End => ActionScope::BufferEnd,
_ => ActionScope::None,
};
let other_mask = match event.key {
Key::Home => ActionScope::BackwardLine,
Key::End => ActionScope::ForwardLine,
_ => ActionScope::None,
};
match event.modifier {
ModifierType::CONTROL_MASK => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::MoveCursor, ctrl_mask);
}
ModifierType::SHIFT_MASK => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::Select, other_mask);
}
_ => {
tool_update_result =
Self::handle_text_buffer_action(t, Action::MoveCursor, other_mask);
}
}
}
Key::a | Key::A => {
if event.modifier == ModifierType::CONTROL_MASK {
tool_update_result = Self::handle_text_buffer_action(
t,
Action::Select,
ActionScope::SelectAll,
);
}
}
Key::v | Key::V => {
let display = DisplayManager::get().default_display();
if display.is_none() {
eprintln!("Cannot open default display for clipboard.");
return ToolUpdateResult::StopPropagation;
}
let clipboard = display.unwrap().clipboard();
let buffer = t.text_buffer.clone();
Self::handle_text_buffer_action(t, Action::Delete, ActionScope::None);
let sender = self.sender.clone();
relm4::gtk::glib::MainContext::default().spawn_local(async move {
match clipboard.read_text_future().await {
Ok(Some(text)) => {
buffer.insert_at_cursor(&text);
if let Some(sender) = sender {
sender.emit(SketchBoardInput::Refresh);
}
}
Ok(None) => {
eprintln!("Clipboard contains no text");
}
Err(err) => {
eprintln!("Clipboard read error: {}", err);
}
}
});
}
Key::c | Key::C => {
if event.modifier == ModifierType::CONTROL_MASK
&& let Some(text) = &self.text
{
let buffer = text.text_buffer.clone();
if let Some((start, end)) = buffer.selection_bounds() {
let selected_text = buffer.text(&start, &end, false);
let display = DisplayManager::get().default_display();
if display.is_none() {
eprintln!("Cannot open default display for clipboard.");
return ToolUpdateResult::StopPropagation;
}
let clipboard = display.unwrap().clipboard();
clipboard.set_text(&selected_text);
}
}
}
Key::x | Key::X => {
if event.modifier == ModifierType::CONTROL_MASK
&& let Some(text) = &mut self.text
{
let buffer = text.text_buffer.clone();
if let Some((start, end)) = buffer.selection_bounds() {
let selected_text = buffer.text(&start, &end, false);
let display = DisplayManager::get().default_display();
if display.is_none() {
eprintln!("Cannot open default display for clipboard.");
return ToolUpdateResult::StopPropagation;
}
let clipboard = display.unwrap().clipboard();
clipboard.set_text(&selected_text);
Self::handle_text_buffer_action(
text,
Action::Delete,
ActionScope::None,
);
tool_update_result = ToolUpdateResult::RedrawAndStopPropagation;
}
}
}
Key::Insert => {
if event.modifier == ModifierType::SHIFT_MASK {
let display = DisplayManager::get().default_display();
if display.is_none() {
eprintln!("Cannot open default display for clipboard.");
return ToolUpdateResult::StopPropagation;
}
let selection_clipboard = display.unwrap().primary_clipboard();
let buffer = t.text_buffer.clone();
Self::handle_text_buffer_action(t, Action::Delete, ActionScope::None);
let sender = self.sender.clone();
relm4::gtk::glib::MainContext::default().spawn_local(async move {
match selection_clipboard.read_text_future().await {
Ok(Some(text)) => {
buffer.insert_at_cursor(&text);
if let Some(sender) = sender {
sender.emit(SketchBoardInput::Refresh);
}
}
Ok(None) => {
eprintln!("selection_clipboard contains no text");
}
Err(err) => {
eprintln!("selection_clipboard read error: {}", err);
}
}
});
}
}
_ => {
tool_update_result = ToolUpdateResult::Unmodified;
}
}
} else {
tool_update_result = ToolUpdateResult::Unmodified;
}
tool_update_result
}
fn handle_key_release_event(&mut self, event: KeyEventMsg) -> ToolUpdateResult {
if (event.key == Key::Alt_L || event.key == Key::Alt_R) && self.alt_tap {
self.alt_tap = false;
if let Some(t) = &mut self.text {
t.effect = t.effect.next();
return ToolUpdateResult::RedrawAndStopPropagation;
}
}
ToolUpdateResult::Unmodified
}
fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult {
match event.type_ {
MouseEventType::Click => {
match event.button {
MouseButton::Primary => {
let pos = event.pos;
if let Some(t) = &mut self.text {
let rect = t.rect.borrow();
if rect.contains_point(pos.x as i32, pos.y as i32) {
let mut index = 0;
let mut find_index = false;
let glyphs = t.glyphs.borrow();
for line in 0..glyphs.len() {
let line_rect = glyphs.get(line).unwrap();
for glyph in line_rect.iter() {
if glyph.contains_point(pos.x as i32, pos.y as i32) {
find_index = true;
if pos.x > glyph.x() as f32 + glyph.width() as f32 / 2.0
{
index += 1;
}
break;
}
index += 1;
}
if find_index {
break;
}
let first_ele = line_rect.iter().next().unwrap();
if pos.y <= (first_ele.y() + first_ele.height()) as f32
&& line != glyphs.len() - 1
{
index -= 1;
break;
}
}
let buffer = &t.text_buffer;
let mut cursor_iter = buffer.iter_at_mark(&buffer.get_insert());
cursor_iter.set_offset(index);
t.text_buffer.place_cursor(&cursor_iter);
if event.n_pressed == 2 {
let mut start_itr = cursor_iter;
let mut end_itr = start_itr;
start_itr.backward_word_start();
end_itr.forward_word_end();
t.text_buffer.select_range(&start_itr, &end_itr);
} else if event.n_pressed == 3 {
let mut start_itr = cursor_iter;
let mut end_itr = start_itr;
while !start_itr.is_start() {
start_itr.backward_line();
}
end_itr.forward_to_end();
t.text_buffer.select_range(&start_itr, &end_itr);
}
return ToolUpdateResult::RedrawAndStopPropagation;
}
}
let return_value = match &mut self.text {
Some(l) => {
let content = l.get_text();
if content.is_empty() {
ToolUpdateResult::Redraw
} else {
l.preedit = None;
l.editing = false;
l.im_context = None;
l.text_buffer.select_range(
&l.text_buffer.start_iter(),
&l.text_buffer.start_iter(),
);
*l.draw_rect.borrow_mut() = false;
ToolUpdateResult::Commit(l.clone_box())
}
}
None => ToolUpdateResult::Redraw,
};
self.text = Some(Text::new(event.pos, self.style, self.im_context.clone()));
self.set_input_enabled(true);
return_value
}
_ => ToolUpdateResult::Unmodified,
}
}
MouseEventType::Release => match event.button {
MouseButton::Middle => {
if let Some(t) = &mut self.text {
let display = DisplayManager::get().default_display();
if display.is_none() {
eprintln!("Cannot open default display for clipboard.");
return ToolUpdateResult::StopPropagation;
}
let selection_clipboard = display.unwrap().primary_clipboard();
let buffer = t.text_buffer.clone();
Self::handle_text_buffer_action(t, Action::Delete, ActionScope::None);
let sender = self.sender.clone();
let dragged = self.dragged.clone();
relm4::gtk::glib::MainContext::default().spawn_local(async move {
match selection_clipboard.read_text_future().await {
Ok(Some(text)) => {
if !*dragged.borrow() {
buffer.insert_at_cursor(&text);
if let Some(sender) = sender {
sender.emit(SketchBoardInput::Refresh);
}
}
}
Ok(None) => {
eprintln!("selection_clipboard contains no text");
}
Err(err) => {
eprintln!("selection_clipboard read error: {}", err);
}
}
});
}
ToolUpdateResult::StopPropagation
}
_ => ToolUpdateResult::Unmodified,
},
MouseEventType::BeginDrag => {
self.drag_start_pos = event.pos;
if let Some(t) = &mut self.text {
let rect = t.rect.borrow();
if rect.contains_point(event.pos.x as i32, event.pos.y as i32) {
return ToolUpdateResult::StopPropagation;
}
}
ToolUpdateResult::Unmodified
}
MouseEventType::UpdateDrag => {
self.dragged = Rc::new(RefCell::new(true));
if event.button == MouseButton::Primary {
let global_pos = self.drag_start_pos + event.pos;
if let Some(t) = &mut self.text {
let rect = t.rect.borrow();
if rect.contains_point(global_pos.x as i32, global_pos.y as i32) {
let mut index = 0;
let mut find_index = false;
let glyphs = t.glyphs.borrow();
for line in glyphs.iter() {
for glyph in line.iter() {
if glyph
.contains_point(global_pos.x as i32, global_pos.y as i32)
{
find_index = true;
if global_pos.x
> glyph.x() as f32 + glyph.width() as f32 / 2.0
{
index += 1;
}
break;
}
index += 1;
}
let first_ele = line.iter().next().unwrap();
if find_index
|| global_pos.y <= (first_ele.y() + first_ele.height()) as f32
{
break;
}
}
let buffer = &t.text_buffer;
let mut cursor_iter = buffer.iter_at_mark(&buffer.get_insert());
cursor_iter.set_offset(index);
let start_cursor_itr = buffer.iter_at_mark(&buffer.get_insert());
buffer.select_range(&start_cursor_itr, &cursor_iter);
return ToolUpdateResult::RedrawAndStopPropagation;
}
}
return ToolUpdateResult::StopPropagation;
}
ToolUpdateResult::Unmodified
}
MouseEventType::EndDrag => {
self.dragged = Rc::new(RefCell::new(false));
if let Some(t) = &mut self.text {
let rect = t.rect.borrow();
if rect.contains_point(event.pos.x as i32, event.pos.y as i32) {
return ToolUpdateResult::StopPropagation;
}
}
ToolUpdateResult::Unmodified
}
_ => ToolUpdateResult::Unmodified,
}
}
fn handle_deactivated(&mut self) -> ToolUpdateResult {
self.input_enabled = false;
if let Some(t) = &mut self.text {
let content = t.get_text();
if content.is_empty() {
self.text = None;
ToolUpdateResult::Redraw
} else {
t.preedit = None;
t.editing = false;
t.im_context = None;
t.text_buffer
.select_range(&t.text_buffer.start_iter(), &t.text_buffer.start_iter());
*t.draw_rect.borrow_mut() = false;
let result = t.clone_box();
self.text = None;
self.input_enabled = false;
ToolUpdateResult::Commit(result)
}
} else {
ToolUpdateResult::Unmodified
}
}
fn handle_dismissed(&mut self) -> ToolUpdateResult {
self.input_enabled = false;
self.text = None;
ToolUpdateResult::Redraw
}
fn active(&self) -> bool {
self.text.is_some()
}
fn handle_undo(&mut self) -> ToolUpdateResult {
if let Some(t) = &self.text {
t.text_buffer.undo();
ToolUpdateResult::Redraw
} else {
ToolUpdateResult::Unmodified
}
}
fn handle_redo(&mut self) -> ToolUpdateResult {
if let Some(t) = &self.text {
t.text_buffer.redo();
ToolUpdateResult::Redraw
} else {
ToolUpdateResult::Unmodified
}
}
fn set_sender(&mut self, sender: Sender<SketchBoardInput>) {
self.sender = Some(sender);
}
}
enum ActionScope {
ForwardChar,
BackwardChar,
ForwardLine,
BackwardLine,
ForwardWord,
BackwardWord,
ForwardLineAndWord,
BackwardLineAndWord,
SelectAll,
BufferStart,
BufferEnd,
Left,
Right,
Up,
Down,
None,
}
enum Action {
Delete,
MoveCursor,
Select,
MoveOrigin,
NudgeOrigin,
}
impl TextTool {
fn handle_text_buffer_action(
text: &mut Text,
action: Action,
action_scope: ActionScope,
) -> ToolUpdateResult {
let text_buffer = &text.text_buffer;
let mut start_cursor_itr = text_buffer.iter_at_mark(&text_buffer.get_insert());
match action {
Action::Delete => {
let mut end_cursor_itr = start_cursor_itr;
if let Some((start, end)) = text_buffer.selection_bounds() {
start_cursor_itr = start;
end_cursor_itr = end;
} else {
match action_scope {
ActionScope::ForwardChar => end_cursor_itr.forward_char(),
ActionScope::BackwardChar => end_cursor_itr.backward_char(),
ActionScope::ForwardWord => end_cursor_itr.forward_word_end(),
ActionScope::BackwardWord => end_cursor_itr.backward_word_start(),
_ => false, };
}
if text_buffer.delete_interactive(&mut start_cursor_itr, &mut end_cursor_itr, true)
{
ToolUpdateResult::RedrawAndStopPropagation
} else {
ToolUpdateResult::StopPropagation
}
}
Action::MoveCursor => {
let mut cursor_itr = start_cursor_itr;
let mut start_iter = None;
let mut end_iter = None;
let mut has_selection = false;
if let Some((start, end)) = text_buffer.selection_bounds() {
start_iter = Some(start);
end_iter = Some(end);
has_selection = true;
}
match action_scope {
ActionScope::ForwardChar => {
if has_selection {
cursor_itr = end_iter.unwrap();
false
} else {
cursor_itr.forward_char()
}
}
ActionScope::BackwardChar => {
if has_selection {
cursor_itr = start_iter.unwrap();
false
} else {
cursor_itr.backward_char()
}
}
ActionScope::ForwardLine => cursor_itr.forward_to_line_end(),
ActionScope::ForwardWord => cursor_itr.forward_word_end(),
ActionScope::BackwardWord => cursor_itr.backward_word_start(),
ActionScope::BackwardLine => {
if cursor_itr.starts_line() {
cursor_itr.backward_line()
} else {
while !cursor_itr.starts_line() {
cursor_itr.backward_char();
}
false
}
}
ActionScope::BufferEnd => {
cursor_itr.forward_to_end();
false
}
ActionScope::BufferStart => {
while !cursor_itr.is_start() {
cursor_itr.backward_line();
}
false
}
ActionScope::ForwardLineAndWord => {
if has_selection {
cursor_itr = end_iter.unwrap();
} else {
let content = &text.get_text();
let current_offset = cursor_itr.offset();
let mut next_line = 0;
let mut offset = 0;
let ranges = text.line_ranges.borrow();
for i in 0..ranges.len() {
let line = ranges.get(i).unwrap();
let start = content[..line.start].chars().count();
let end = content[..line.end].chars().count();
if current_offset >= start as i32 && current_offset <= end as i32 {
offset = if i == ranges.len() - 1 {
(end - start) as i32
} else {
let temp = current_offset - start as i32;
let next_start = content
[..ranges.get(i + 1).unwrap().start]
.chars()
.count();
let next_end = content[..ranges.get(i + 1).unwrap().end]
.chars()
.count();
let limit = (next_end - next_start) as i32;
if temp > limit { limit } else { temp }
};
next_line = if i == ranges.len() - 1 {
content[..ranges.get(i).unwrap().start].chars().count()
as i32
} else {
content[..ranges.get(i + 1).unwrap().start].chars().count()
as i32
};
break;
}
}
let move_offset = next_line + offset;
cursor_itr.set_offset(move_offset);
}
false
}
ActionScope::BackwardLineAndWord => {
if has_selection {
cursor_itr = start_iter.unwrap();
} else {
let content = &text.get_text();
let current_offset = cursor_itr.offset();
let mut last_line = 0;
let mut offset = 0;
let ranges = text.line_ranges.borrow();
for i in 0..ranges.len() {
let line = ranges.get(i).unwrap();
let start = content[..line.start].chars().count();
let end = content[..line.end].chars().count();
if current_offset >= start as i32 && current_offset <= end as i32 {
offset = if i == 0 {
0
} else {
let temp = current_offset - start as i32;
let last_start = content
[..ranges.get(i - 1).unwrap().start]
.chars()
.count();
let last_end = content[..ranges.get(i - 1).unwrap().end]
.chars()
.count();
let limit = (last_end - last_start) as i32;
if temp > limit { limit } else { temp }
};
last_line = if i == 0 {
content[..ranges.get(i).unwrap().start].chars().count()
as i32
} else {
content[..ranges.get(i - 1).unwrap().start].chars().count()
as i32
};
break;
}
}
let move_offset = last_line + offset;
cursor_itr.set_offset(move_offset);
}
false
}
_ => false, };
text_buffer.select_range(&text_buffer.start_iter(), &text_buffer.start_iter());
text_buffer.place_cursor(&cursor_itr);
let new_cursor_itr = text_buffer.iter_at_mark(&text_buffer.get_insert());
if new_cursor_itr != start_cursor_itr || has_selection {
ToolUpdateResult::RedrawAndStopPropagation
} else {
ToolUpdateResult::StopPropagation
}
}
Action::Select => {
let mut start_cursor_itr_new = start_cursor_itr;
let mut end_cursor_itr = start_cursor_itr;
if let Some((start, end)) = text_buffer.selection_bounds() {
let insert = text_buffer.get_insert();
let insert_iter = text_buffer.iter_at_mark(&insert);
if insert_iter == start {
start_cursor_itr_new = start;
end_cursor_itr = end;
} else {
start_cursor_itr_new = end;
end_cursor_itr = start;
}
}
match action_scope {
ActionScope::ForwardChar => {
end_cursor_itr.forward_char();
}
ActionScope::BackwardChar => {
end_cursor_itr.backward_char();
}
ActionScope::ForwardLine => {
end_cursor_itr.forward_to_line_end();
}
ActionScope::BackwardLine => {
if end_cursor_itr.starts_line() {
end_cursor_itr.backward_line();
} else {
while !end_cursor_itr.starts_line() {
end_cursor_itr.backward_char();
}
}
}
ActionScope::ForwardLineAndWord => {
let content = &text.get_text();
let current_offset = end_cursor_itr.offset();
let mut next_line = 0;
let mut offset = 0;
let ranges = text.line_ranges.borrow();
for i in 0..ranges.len() {
let line = ranges.get(i).unwrap();
let start = content[..line.start].chars().count();
let end = content[..line.end].chars().count();
if current_offset >= start as i32 && current_offset <= end as i32 {
offset = if i == ranges.len() - 1 {
(end - start) as i32
} else {
let temp = current_offset - start as i32;
let next_start =
content[..ranges.get(i + 1).unwrap().start].chars().count();
let next_end =
content[..ranges.get(i + 1).unwrap().end].chars().count();
let limit = (next_end - next_start) as i32;
if temp > limit { limit } else { temp }
};
next_line = if i == ranges.len() - 1 {
content[..ranges.get(i).unwrap().start].chars().count() as i32
} else {
content[..ranges.get(i + 1).unwrap().start].chars().count()
as i32
};
break;
}
}
let move_offset = next_line + offset;
end_cursor_itr.set_offset(move_offset);
}
ActionScope::BackwardLineAndWord => {
let content = &text.get_text();
let current_offset = end_cursor_itr.offset();
let mut last_line = 0;
let mut offset = 0;
let ranges = text.line_ranges.borrow();
for i in 0..ranges.len() {
let line = ranges.get(i).unwrap();
let start = content[..line.start].chars().count();
let end = content[..line.end].chars().count();
if current_offset >= start as i32 && current_offset <= end as i32 {
offset = if i == 0 {
0
} else {
let temp = current_offset - start as i32;
let last_start =
content[..ranges.get(i - 1).unwrap().start].chars().count();
let last_end =
content[..ranges.get(i - 1).unwrap().end].chars().count();
let limit = (last_end - last_start) as i32;
if temp > limit { limit } else { temp }
};
last_line = if i == 0 {
content[..ranges.get(i).unwrap().start].chars().count() as i32
} else {
content[..ranges.get(i - 1).unwrap().start].chars().count()
as i32
};
break;
}
}
let move_offset = last_line + offset;
end_cursor_itr.set_offset(move_offset);
}
ActionScope::ForwardWord => {
end_cursor_itr.forward_word_end();
}
ActionScope::BackwardWord => {
end_cursor_itr.backward_word_start();
}
ActionScope::SelectAll => {
start_cursor_itr_new = text_buffer.start_iter();
end_cursor_itr = text_buffer.end_iter();
}
_ => {}
}
text_buffer.select_range(&start_cursor_itr_new, &end_cursor_itr);
ToolUpdateResult::RedrawAndStopPropagation
}
Action::MoveOrigin | Action::NudgeOrigin => {
let length = match action {
Action::MoveOrigin => APP_CONFIG.read().text_move_length(),
Action::NudgeOrigin => 1.0,
_ => 0.0,
};
let offset = match action_scope {
ActionScope::Left => Vec2D::new(-length, 0.0),
ActionScope::Right => Vec2D::new(length, 0.0),
ActionScope::Up => Vec2D::new(0.0, -length),
ActionScope::Down => Vec2D::new(0.0, length),
_ => Vec2D::new(0.0, 0.0),
};
if offset.is_zero() {
ToolUpdateResult::StopPropagation
} else {
text.pos += offset;
ToolUpdateResult::RedrawAndStopPropagation
}
}
}
}
}