lapce--lapce
802 行
26 KiB
Rust
802 行
26 KiB
Rust
use std::{rc::Rc, sync::Arc};
|
|
|
|
use floem::{
|
|
Renderer, View, ViewId,
|
|
action::{set_ime_allowed, set_ime_cursor_area},
|
|
context::EventCx,
|
|
event::{Event, EventListener, EventPropagation},
|
|
kurbo::Stroke,
|
|
peniko::{
|
|
Color,
|
|
kurbo::{Line, Point, Rect, Size, Vec2},
|
|
},
|
|
prop_extractor,
|
|
reactive::{
|
|
Memo, ReadSignal, RwSignal, Scope, SignalGet, SignalUpdate, SignalWith,
|
|
create_effect, create_memo, create_rw_signal,
|
|
},
|
|
style::{
|
|
CursorStyle, FontFamily, FontSize, FontStyle, FontWeight, LineHeight,
|
|
PaddingLeft, Style, TextColor,
|
|
},
|
|
taffy::prelude::NodeId,
|
|
text::{Attrs, AttrsList, FamilyOwned, TextLayout},
|
|
unit::PxPct,
|
|
views::Decorators,
|
|
};
|
|
use lapce_core::{
|
|
buffer::rope_text::RopeText,
|
|
cursor::{Cursor, CursorMode},
|
|
selection::Selection,
|
|
};
|
|
use lapce_xi_rope::Rope;
|
|
|
|
use crate::{
|
|
config::{LapceConfig, color::LapceColor},
|
|
doc::Doc,
|
|
editor::{DocSignal, EditorData, view::editor_style},
|
|
keypress::KeyPressFocus,
|
|
main_split::Editors,
|
|
window_tab::CommonData,
|
|
};
|
|
|
|
prop_extractor! {
|
|
Extractor {
|
|
color: TextColor,
|
|
font_size: FontSize,
|
|
font_family: FontFamily,
|
|
font_weight: FontWeight,
|
|
font_style: FontStyle,
|
|
line_height: LineHeight,
|
|
}
|
|
}
|
|
|
|
/// Builder for creating a [`TextInput`] easily.
|
|
pub struct TextInputBuilder {
|
|
is_focused: Option<Memo<bool>>,
|
|
// TODO: it'd be nice to not need to box this
|
|
key_focus: Option<Box<dyn KeyPressFocus>>,
|
|
value: Option<Rope>,
|
|
keyboard_focus: RwSignal<bool>,
|
|
}
|
|
|
|
impl Default for TextInputBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl TextInputBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
is_focused: None,
|
|
key_focus: None,
|
|
value: None,
|
|
keyboard_focus: create_rw_signal(false),
|
|
}
|
|
}
|
|
|
|
pub fn is_focused(mut self, is_focused: impl Fn() -> bool + 'static) -> Self {
|
|
let keyboard_focus = self.keyboard_focus;
|
|
self.is_focused =
|
|
Some(create_memo(move |_| is_focused() || keyboard_focus.get()));
|
|
self
|
|
}
|
|
|
|
/// Initialize with a specific value.
|
|
/// If this is set it will apply the value via reloading the editor's doc as pristine.
|
|
pub fn value(mut self, value: impl Into<Rope>) -> Self {
|
|
self.value = Some(value.into());
|
|
self
|
|
}
|
|
|
|
pub fn key_focus(mut self, key_focus: impl KeyPressFocus + 'static) -> Self {
|
|
self.key_focus = Some(Box::new(key_focus));
|
|
self
|
|
}
|
|
|
|
pub fn build(
|
|
self,
|
|
cx: Scope,
|
|
editors: Editors,
|
|
common: Rc<CommonData>,
|
|
) -> TextInput {
|
|
let editor = editors.make_local(cx, common);
|
|
let id = editor.id();
|
|
|
|
self.build_editor(editor).on_cleanup(move || {
|
|
editors.remove(id);
|
|
})
|
|
}
|
|
|
|
/// Build the text input with a specific editor.
|
|
/// This function does *not* perform add/cleanup the editor to/from [`Editors`]
|
|
pub fn build_editor(self, editor: EditorData) -> TextInput {
|
|
let keyboard_focus = self.keyboard_focus;
|
|
let is_focused = if let Some(is_focused) = self.is_focused {
|
|
is_focused
|
|
} else {
|
|
create_memo(move |_| keyboard_focus.get())
|
|
};
|
|
|
|
if let Some(value) = self.value {
|
|
editor.doc().reload(value, true);
|
|
}
|
|
|
|
text_input_full(editor, self.key_focus, is_focused, keyboard_focus)
|
|
}
|
|
}
|
|
|
|
/// Create a basic single line text input
|
|
/// `e_data` is the editor data that this input is associated with.
|
|
/// `supplied_editor`
|
|
/// `key_focus` is what receives the keydown events, leave as `None` to default to editor.
|
|
/// `is_focused` is a function that returns if the input is focused, used for certain events.
|
|
fn text_input_full<T: KeyPressFocus + 'static>(
|
|
e_data: EditorData,
|
|
key_focus: Option<T>,
|
|
is_focused: Memo<bool>,
|
|
keyboard_focus: RwSignal<bool>,
|
|
) -> TextInput {
|
|
let id = ViewId::new();
|
|
|
|
let doc = e_data.doc_signal();
|
|
let cursor = e_data.cursor();
|
|
let config = e_data.common.config;
|
|
let keypress = e_data.common.keypress;
|
|
let window_origin = create_rw_signal(Point::ZERO);
|
|
let cursor_line = create_rw_signal(Line::new(Point::ZERO, Point::ZERO));
|
|
let local_editor = e_data.clone();
|
|
let editor = local_editor.editor.clone();
|
|
|
|
{
|
|
let doc = doc.get();
|
|
create_effect(move |_| {
|
|
let offset = cursor.with(|c| c.offset());
|
|
let (content, offset, preedit_range) = {
|
|
let content = doc.buffer.with(|b| b.to_string());
|
|
if let Some(preedit) = doc.preedit.preedit.get().as_ref() {
|
|
let mut new_content = String::new();
|
|
new_content.push_str(&content[..offset]);
|
|
new_content.push_str(&preedit.text);
|
|
new_content.push_str(&content[offset..]);
|
|
let range = (offset, offset + preedit.text.len());
|
|
let offset = preedit
|
|
.cursor
|
|
.as_ref()
|
|
.map(|(_, end)| offset + *end)
|
|
.unwrap_or(offset);
|
|
(new_content, offset, Some(range))
|
|
} else {
|
|
(content, offset, None)
|
|
}
|
|
};
|
|
id.update_state(TextInputState::Content {
|
|
text: content,
|
|
offset,
|
|
preedit_range,
|
|
});
|
|
});
|
|
}
|
|
|
|
{
|
|
create_effect(move |_| {
|
|
let focus = is_focused.get();
|
|
id.update_state(TextInputState::Focus(focus));
|
|
});
|
|
|
|
let editor = editor.clone();
|
|
let ime_allowed = editor.ime_allowed;
|
|
create_effect(move |_| {
|
|
let focus = is_focused.get();
|
|
if focus {
|
|
if !ime_allowed.get_untracked() {
|
|
ime_allowed.set(true);
|
|
set_ime_allowed(true);
|
|
}
|
|
let cursor_line = cursor_line.get();
|
|
|
|
let window_origin = window_origin.get();
|
|
let viewport = editor.viewport.get();
|
|
let origin = window_origin
|
|
+ Vec2::new(
|
|
cursor_line.p1.x - viewport.x0,
|
|
cursor_line.p1.y - viewport.y0,
|
|
);
|
|
set_ime_cursor_area(origin, Size::new(800.0, 600.0));
|
|
}
|
|
});
|
|
}
|
|
|
|
let common_keyboard_focus = e_data.common.keyboard_focus;
|
|
|
|
let ed1 = editor.clone();
|
|
let ed2 = editor.clone();
|
|
TextInput {
|
|
id,
|
|
config,
|
|
offset: 0,
|
|
preedit_range: None,
|
|
layout_rect: Rect::ZERO,
|
|
content: "".to_string(),
|
|
focus: false,
|
|
text_node: None,
|
|
text_layout: create_rw_signal(None),
|
|
text_rect: Rect::ZERO,
|
|
text_viewport: Rect::ZERO,
|
|
cursor_line,
|
|
placeholder: "".to_string(),
|
|
placeholder_text_layout: None,
|
|
editor: e_data.clone(),
|
|
cursor_pos: Point::ZERO,
|
|
on_cursor_pos: None,
|
|
hide_cursor: editor.cursor_info.hidden,
|
|
style: Default::default(),
|
|
}
|
|
.style(move |s| {
|
|
editor_style(config, doc, s)
|
|
.cursor(CursorStyle::Text)
|
|
.padding_horiz(10.0)
|
|
.padding_vert(6.0)
|
|
})
|
|
.on_move(move |pos| {
|
|
window_origin.set(pos);
|
|
})
|
|
.on_event_stop(EventListener::FocusGained, move |_| {
|
|
keyboard_focus.set(true);
|
|
common_keyboard_focus.set(Some(id));
|
|
})
|
|
.on_event_stop(EventListener::FocusLost, move |_| {
|
|
keyboard_focus.set(false);
|
|
if common_keyboard_focus.get_untracked() == Some(id) {
|
|
common_keyboard_focus.set(None);
|
|
}
|
|
})
|
|
.on_event(EventListener::KeyDown, move |event| {
|
|
if let Event::KeyDown(key_event) = event {
|
|
let keypress = keypress.get_untracked();
|
|
let key_focus = key_focus
|
|
.as_ref()
|
|
.map(|k| k as &dyn KeyPressFocus)
|
|
.unwrap_or(&e_data);
|
|
if keypress.key_down(key_event, key_focus).handled {
|
|
EventPropagation::Stop
|
|
} else {
|
|
EventPropagation::Continue
|
|
}
|
|
} else {
|
|
EventPropagation::Continue
|
|
}
|
|
})
|
|
.on_event(EventListener::ImePreedit, move |event| {
|
|
if !is_focused.get_untracked() {
|
|
return EventPropagation::Continue;
|
|
}
|
|
|
|
if let Event::ImePreedit {
|
|
text,
|
|
cursor: ime_cursor,
|
|
} = event
|
|
{
|
|
if text.is_empty() {
|
|
ed1.clear_preedit();
|
|
} else {
|
|
let offset = cursor.with_untracked(|c| c.offset());
|
|
ed1.set_preedit(text.clone(), ime_cursor.to_owned(), offset);
|
|
}
|
|
}
|
|
EventPropagation::Stop
|
|
})
|
|
.on_event(EventListener::ImeCommit, move |event| {
|
|
if !is_focused.get_untracked() {
|
|
return EventPropagation::Continue;
|
|
}
|
|
|
|
if let Event::ImeCommit(text) = event {
|
|
ed2.clear_preedit();
|
|
ed2.receive_char(text.as_str());
|
|
}
|
|
EventPropagation::Stop
|
|
})
|
|
}
|
|
|
|
enum TextInputState {
|
|
Content {
|
|
text: String,
|
|
offset: usize,
|
|
preedit_range: Option<(usize, usize)>,
|
|
},
|
|
Focus(bool),
|
|
Placeholder(String),
|
|
}
|
|
|
|
pub struct TextInput {
|
|
id: ViewId,
|
|
content: String,
|
|
offset: usize,
|
|
preedit_range: Option<(usize, usize)>,
|
|
editor: EditorData,
|
|
focus: bool,
|
|
text_node: Option<NodeId>,
|
|
text_layout: RwSignal<Option<TextLayout>>,
|
|
text_rect: Rect,
|
|
text_viewport: Rect,
|
|
layout_rect: Rect,
|
|
cursor_line: RwSignal<Line>,
|
|
placeholder: String,
|
|
placeholder_text_layout: Option<TextLayout>,
|
|
cursor_pos: Point,
|
|
on_cursor_pos: Option<Box<dyn Fn(Point)>>,
|
|
hide_cursor: RwSignal<bool>,
|
|
config: ReadSignal<Arc<LapceConfig>>,
|
|
style: Extractor,
|
|
}
|
|
|
|
impl TextInput {
|
|
pub fn placeholder(self, placeholder: impl Fn() -> String + 'static) -> Self {
|
|
let id = self.id;
|
|
create_effect(move |_| {
|
|
let placeholder = placeholder();
|
|
id.update_state(TextInputState::Placeholder(placeholder));
|
|
});
|
|
self
|
|
}
|
|
|
|
pub fn on_cursor_pos(mut self, cursor_pos: impl Fn(Point) + 'static) -> Self {
|
|
self.on_cursor_pos = Some(Box::new(cursor_pos));
|
|
self
|
|
}
|
|
|
|
pub fn editor(&self) -> EditorData {
|
|
self.editor.clone()
|
|
}
|
|
|
|
pub fn doc_signal(&self) -> DocSignal {
|
|
self.editor.doc_signal()
|
|
}
|
|
|
|
pub fn doc(&self) -> Rc<Doc> {
|
|
self.editor.doc()
|
|
}
|
|
|
|
pub fn cursor(&self) -> RwSignal<Cursor> {
|
|
self.editor.cursor()
|
|
}
|
|
|
|
fn set_text_layout(&mut self) {
|
|
let mut text_layout = TextLayout::new();
|
|
let mut attrs =
|
|
Attrs::new().color(self.style.color().unwrap_or(Color::BLACK));
|
|
if let Some(font_size) = self.style.font_size() {
|
|
attrs = attrs.font_size(font_size);
|
|
}
|
|
if let Some(font_style) = self.style.font_style() {
|
|
attrs = attrs.style(font_style);
|
|
}
|
|
let font_family = self.style.font_family().as_ref().map(|font_family| {
|
|
let family: Vec<FamilyOwned> =
|
|
FamilyOwned::parse_list(font_family).collect();
|
|
family
|
|
});
|
|
if let Some(font_family) = font_family.as_ref() {
|
|
attrs = attrs.family(font_family);
|
|
}
|
|
if let Some(font_weight) = self.style.font_weight() {
|
|
attrs = attrs.weight(font_weight);
|
|
}
|
|
if let Some(line_height) = self.style.line_height() {
|
|
attrs = attrs.line_height(line_height);
|
|
}
|
|
text_layout.set_text(
|
|
if self.content.is_empty() {
|
|
" "
|
|
} else {
|
|
self.content.as_str()
|
|
},
|
|
AttrsList::new(attrs.clone()),
|
|
None,
|
|
);
|
|
self.text_layout.set(Some(text_layout));
|
|
|
|
let mut placeholder_text_layout = TextLayout::new();
|
|
attrs = attrs.color(
|
|
self.style
|
|
.color()
|
|
.unwrap_or(Color::BLACK)
|
|
.multiply_alpha(0.5),
|
|
);
|
|
placeholder_text_layout.set_text(
|
|
&self.placeholder,
|
|
AttrsList::new(attrs),
|
|
None,
|
|
);
|
|
self.placeholder_text_layout = Some(placeholder_text_layout);
|
|
}
|
|
|
|
fn hit_index(&self, _cx: &mut EventCx, point: Point) -> usize {
|
|
self.text_layout.with_untracked(|text_layout| {
|
|
if let Some(text_layout) = text_layout.as_ref() {
|
|
let padding_left =
|
|
match self.id.get_combined_style().get(PaddingLeft) {
|
|
PxPct::Px(v) => v,
|
|
PxPct::Pct(pct) => {
|
|
let layout = self.id.get_layout().unwrap_or_default();
|
|
pct * layout.size.width as f64
|
|
}
|
|
};
|
|
let hit =
|
|
text_layout.hit_point(Point::new(point.x - padding_left, 0.0));
|
|
hit.index.min(self.content.len())
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
}
|
|
|
|
fn clamp_text_viewport(&mut self, text_viewport: Rect) {
|
|
let text_rect = self.text_rect;
|
|
let actual_size = text_rect.size();
|
|
let width = text_rect.width();
|
|
let height = text_rect.height();
|
|
let child_size = self
|
|
.text_layout
|
|
.with_untracked(|text_layout| text_layout.as_ref().unwrap().size());
|
|
|
|
let mut text_viewport = text_viewport;
|
|
if width >= child_size.width {
|
|
text_viewport.x0 = 0.0;
|
|
} else if text_viewport.x0 > child_size.width - width {
|
|
text_viewport.x0 = child_size.width - width;
|
|
} else if text_viewport.x0 < 0.0 {
|
|
text_viewport.x0 = 0.0;
|
|
}
|
|
|
|
if height >= child_size.height {
|
|
text_viewport.y0 = 0.0;
|
|
} else if text_viewport.y0 > child_size.height - height {
|
|
text_viewport.y0 = child_size.height - height;
|
|
} else if text_viewport.y0 < 0.0 {
|
|
text_viewport.y0 = 0.0;
|
|
}
|
|
|
|
let text_viewport = text_viewport.with_size(actual_size);
|
|
if text_viewport != self.text_viewport {
|
|
self.text_viewport = text_viewport;
|
|
self.id.request_paint();
|
|
}
|
|
}
|
|
|
|
fn ensure_cursor_visible(&mut self) {
|
|
fn closest_on_axis(val: f64, min: f64, max: f64) -> f64 {
|
|
assert!(min <= max);
|
|
if val > min && val < max {
|
|
0.0
|
|
} else if val <= min {
|
|
val - min
|
|
} else {
|
|
val - max
|
|
}
|
|
}
|
|
|
|
let rect = Rect::ZERO.with_origin(self.cursor_pos).inflate(10.0, 0.0);
|
|
// clamp the target region size to our own size.
|
|
// this means we will show the portion of the target region that
|
|
// includes the origin.
|
|
let target_size = Size::new(
|
|
rect.width().min(self.text_viewport.width()),
|
|
rect.height().min(self.text_viewport.height()),
|
|
);
|
|
let rect = rect.with_size(target_size);
|
|
|
|
let x0 = closest_on_axis(
|
|
rect.min_x(),
|
|
self.text_viewport.min_x(),
|
|
self.text_viewport.max_x(),
|
|
);
|
|
let x1 = closest_on_axis(
|
|
rect.max_x(),
|
|
self.text_viewport.min_x(),
|
|
self.text_viewport.max_x(),
|
|
);
|
|
let y0 = closest_on_axis(
|
|
rect.min_y(),
|
|
self.text_viewport.min_y(),
|
|
self.text_viewport.max_y(),
|
|
);
|
|
let y1 = closest_on_axis(
|
|
rect.max_y(),
|
|
self.text_viewport.min_y(),
|
|
self.text_viewport.max_y(),
|
|
);
|
|
|
|
let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 };
|
|
let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 };
|
|
let new_origin = self.text_viewport.origin() + Vec2::new(delta_x, delta_y);
|
|
self.clamp_text_viewport(self.text_viewport.with_origin(new_origin));
|
|
}
|
|
}
|
|
|
|
impl View for TextInput {
|
|
fn id(&self) -> ViewId {
|
|
self.id
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
_cx: &mut floem::context::UpdateCx,
|
|
state: Box<dyn std::any::Any>,
|
|
) {
|
|
if let Ok(state) = state.downcast() {
|
|
match *state {
|
|
TextInputState::Content {
|
|
text,
|
|
offset,
|
|
preedit_range,
|
|
} => {
|
|
self.content = text;
|
|
self.offset = offset;
|
|
self.preedit_range = preedit_range;
|
|
self.text_layout.set(None);
|
|
}
|
|
TextInputState::Focus(focus) => {
|
|
self.focus = focus;
|
|
}
|
|
TextInputState::Placeholder(placeholder) => {
|
|
self.placeholder = placeholder;
|
|
self.placeholder_text_layout = None;
|
|
}
|
|
}
|
|
self.id.request_layout();
|
|
}
|
|
}
|
|
|
|
fn style_pass(&mut self, cx: &mut floem::context::StyleCx<'_>) {
|
|
if self.style.read(cx) {
|
|
self.set_text_layout();
|
|
self.id.request_layout();
|
|
}
|
|
}
|
|
|
|
fn layout(
|
|
&mut self,
|
|
cx: &mut floem::context::LayoutCx,
|
|
) -> floem::taffy::prelude::NodeId {
|
|
cx.layout_node(self.id, true, |_cx| {
|
|
if self
|
|
.text_layout
|
|
.with_untracked(|text_layout| text_layout.is_none())
|
|
|| self.placeholder_text_layout.is_none()
|
|
{
|
|
self.set_text_layout();
|
|
}
|
|
|
|
let text_layout = self.text_layout;
|
|
text_layout.with_untracked(|text_layout| {
|
|
let text_layout = text_layout.as_ref().unwrap();
|
|
|
|
let offset = self.cursor().get_untracked().offset();
|
|
let cursor_point = text_layout.hit_position(offset).point;
|
|
if cursor_point != self.cursor_pos {
|
|
self.cursor_pos = cursor_point;
|
|
self.ensure_cursor_visible();
|
|
}
|
|
|
|
let size = text_layout.size();
|
|
let height = size.height as f32;
|
|
|
|
if self.text_node.is_none() {
|
|
self.text_node = Some(self.id.new_taffy_node());
|
|
}
|
|
|
|
let text_node = self.text_node.unwrap();
|
|
|
|
let style = Style::new().height(height).to_taffy_style();
|
|
self.id.set_taffy_style(text_node, style);
|
|
});
|
|
|
|
vec![self.text_node.unwrap()]
|
|
})
|
|
}
|
|
|
|
fn compute_layout(
|
|
&mut self,
|
|
_cx: &mut floem::context::ComputeLayoutCx,
|
|
) -> Option<Rect> {
|
|
let layout = self.id.get_layout().unwrap_or_default();
|
|
|
|
let style = self.id.get_combined_style();
|
|
let style = style.builtin();
|
|
let padding_left = match style.padding_left() {
|
|
PxPct::Px(padding) => padding,
|
|
PxPct::Pct(pct) => pct * layout.size.width as f64,
|
|
};
|
|
let padding_right = match style.padding_right() {
|
|
PxPct::Px(padding) => padding,
|
|
PxPct::Pct(pct) => pct * layout.size.width as f64,
|
|
};
|
|
|
|
let size = Size::new(layout.size.width as f64, layout.size.height as f64);
|
|
let mut text_rect = size.to_rect();
|
|
text_rect.x0 += padding_left;
|
|
text_rect.x1 -= padding_right;
|
|
self.text_rect = text_rect;
|
|
|
|
self.clamp_text_viewport(self.text_viewport);
|
|
|
|
let text_node = self.text_node.unwrap();
|
|
let location = self.id.taffy_layout(text_node).unwrap_or_default().location;
|
|
self.layout_rect = size
|
|
.to_rect()
|
|
.with_origin(Point::new(location.x as f64, location.y as f64));
|
|
let offset = self.cursor().with_untracked(|c| c.offset());
|
|
let cursor_line = self.text_layout.with_untracked(|text_layout| {
|
|
let hit_position = text_layout.as_ref().unwrap().hit_position(offset);
|
|
let point = Point::new(location.x as f64, location.y as f64)
|
|
- self.text_viewport.origin().to_vec2();
|
|
let cursor_point = hit_position.point + point.to_vec2();
|
|
|
|
Line::new(
|
|
Point::new(
|
|
cursor_point.x,
|
|
cursor_point.y - hit_position.glyph_ascent,
|
|
),
|
|
Point::new(
|
|
cursor_point.x,
|
|
cursor_point.y + hit_position.glyph_descent,
|
|
),
|
|
)
|
|
});
|
|
self.cursor_line.set(cursor_line);
|
|
|
|
None
|
|
}
|
|
|
|
fn event_before_children(
|
|
&mut self,
|
|
cx: &mut floem::context::EventCx,
|
|
event: &floem::event::Event,
|
|
) -> EventPropagation {
|
|
let text_offset = self.text_viewport.origin();
|
|
let event = event.clone().offset((-text_offset.x, -text_offset.y));
|
|
match event {
|
|
Event::PointerDown(pointer) => {
|
|
let offset = self.hit_index(cx, pointer.pos);
|
|
self.cursor().update(|cursor| {
|
|
cursor.set_insert(Selection::caret(offset));
|
|
});
|
|
if pointer.button.is_primary() && pointer.count == 2 {
|
|
let offset = self.hit_index(cx, pointer.pos);
|
|
let (start, end) = self
|
|
.doc()
|
|
.buffer
|
|
.with_untracked(|buffer| buffer.select_word(offset));
|
|
self.cursor().update(|cursor| {
|
|
cursor.set_insert(Selection::region(start, end));
|
|
});
|
|
} else if pointer.button.is_primary() && pointer.count == 3 {
|
|
self.cursor().update(|cursor| {
|
|
cursor.set_insert(Selection::region(0, self.content.len()));
|
|
});
|
|
}
|
|
cx.update_active(self.id);
|
|
}
|
|
Event::PointerMove(pointer) => {
|
|
if cx.is_active(self.id) {
|
|
let offset = self.hit_index(cx, pointer.pos);
|
|
self.cursor().update(|cursor| {
|
|
cursor.set_offset(offset, true, false);
|
|
});
|
|
}
|
|
}
|
|
Event::PointerWheel(pointer_event) => {
|
|
let delta = pointer_event.delta;
|
|
let delta = if delta.x == 0.0 && delta.y != 0.0 {
|
|
Vec2::new(delta.y, delta.x)
|
|
} else {
|
|
delta
|
|
};
|
|
self.clamp_text_viewport(self.text_viewport + delta);
|
|
return EventPropagation::Continue;
|
|
}
|
|
_ => {}
|
|
}
|
|
EventPropagation::Continue
|
|
}
|
|
|
|
fn paint(&mut self, cx: &mut floem::context::PaintCx) {
|
|
cx.save();
|
|
cx.clip(&self.text_rect.inflate(1.0, 0.0));
|
|
let text_node = self.text_node.unwrap();
|
|
let location = self.id.taffy_layout(text_node).unwrap_or_default().location;
|
|
let point = Point::new(location.x as f64, location.y as f64)
|
|
- self.text_viewport.origin().to_vec2();
|
|
|
|
self.text_layout.with_untracked(|text_layout| {
|
|
let text_layout = text_layout.as_ref().unwrap();
|
|
let height = text_layout.size().height;
|
|
let config = self.config.get_untracked();
|
|
|
|
let cursor = self.cursor().get_untracked();
|
|
|
|
if let CursorMode::Insert(selection) = &cursor.mode {
|
|
for region in selection.regions() {
|
|
if !region.is_caret() {
|
|
let min = text_layout.hit_position(region.min()).point.x;
|
|
let max = text_layout.hit_position(region.max()).point.x;
|
|
cx.fill(
|
|
&Rect::ZERO
|
|
.with_size(Size::new(max - min, height))
|
|
.with_origin(Point::new(min + point.x, point.y)),
|
|
config.color(LapceColor::EDITOR_SELECTION),
|
|
0.0,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !self.content.is_empty() {
|
|
cx.draw_text(text_layout, point);
|
|
} else if !self.placeholder.is_empty() {
|
|
cx.draw_text(self.placeholder_text_layout.as_ref().unwrap(), point);
|
|
}
|
|
|
|
if let Some((start, end)) = self.preedit_range {
|
|
let start_position = text_layout.hit_position(start);
|
|
let start_point = start_position.point
|
|
+ self.layout_rect.origin().to_vec2()
|
|
- self.text_viewport.origin().to_vec2();
|
|
let end_position = text_layout.hit_position(end);
|
|
let end_point = end_position.point
|
|
+ self.layout_rect.origin().to_vec2()
|
|
- self.text_viewport.origin().to_vec2();
|
|
|
|
let line = Line::new(
|
|
Point::new(
|
|
start_point.x,
|
|
start_point.y + start_position.glyph_descent,
|
|
),
|
|
Point::new(
|
|
end_point.x,
|
|
end_point.y + end_position.glyph_descent,
|
|
),
|
|
);
|
|
cx.stroke(
|
|
&line,
|
|
config.color(LapceColor::EDITOR_FOREGROUND),
|
|
&Stroke::new(1.0),
|
|
);
|
|
}
|
|
|
|
if !self.hide_cursor.get_untracked()
|
|
&& (self.focus || cx.is_focused(self.id))
|
|
{
|
|
cx.clip(&self.text_rect.inflate(2.0, 2.0));
|
|
|
|
let hit_position = text_layout.hit_position(self.offset);
|
|
let cursor_point = hit_position.point
|
|
+ self.layout_rect.origin().to_vec2()
|
|
- self.text_viewport.origin().to_vec2();
|
|
|
|
let line = Line::new(
|
|
Point::new(
|
|
cursor_point.x,
|
|
cursor_point.y - hit_position.glyph_ascent,
|
|
),
|
|
Point::new(
|
|
cursor_point.x,
|
|
cursor_point.y + hit_position.glyph_descent,
|
|
),
|
|
);
|
|
|
|
cx.stroke(
|
|
&line,
|
|
self.config.get_untracked().color(LapceColor::EDITOR_CARET),
|
|
&Stroke::new(2.0),
|
|
);
|
|
}
|
|
|
|
cx.restore();
|
|
});
|
|
}
|
|
}
|