forked from reya/gpui-native-kit
chore: reduce component overhead (#3)
Reviewed-on: reya/gpui-native-kit#3
This commit is contained in:
+127
-106
@@ -1,4 +1,3 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -16,7 +15,10 @@ use gpui::{
|
||||
WrappedLine, actions, canvas, div, fill, point, px,
|
||||
};
|
||||
use gpui_native_icons::IconRole;
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size};
|
||||
use gpui_native_theme::{
|
||||
ActiveTheme as _, BORDER_WIDTH, CARET_WIDTH, ColorTokens, Size, UNDERLINE_WIDTH,
|
||||
WHEEL_LINE_HEIGHT,
|
||||
};
|
||||
use gpui_native_utils::focus_ring;
|
||||
|
||||
use crate::button::{Button, ButtonVariant};
|
||||
@@ -29,7 +31,7 @@ const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
||||
/// How often the focused caret blinks.
|
||||
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
/// Pixel height of one scroll "line" for wheel scrolling.
|
||||
const SCROLL_LINES_HEIGHT: f32 = 20.0;
|
||||
const SCROLL_LINES_HEIGHT: f32 = WHEEL_LINE_HEIGHT;
|
||||
/// The row count a multiline field shows until the app sets one.
|
||||
const DEFAULT_ROWS: usize = 4;
|
||||
/// Maximum undo entries kept; the oldest drops off the front.
|
||||
@@ -270,7 +272,7 @@ impl InputBindings {
|
||||
}
|
||||
|
||||
/// Collects the table into keybindings.
|
||||
pub fn into_bindings(self) -> Vec<KeyBinding> {
|
||||
pub(crate) fn into_bindings(self) -> Vec<KeyBinding> {
|
||||
let mut bindings: Vec<KeyBinding> = [
|
||||
self.escape,
|
||||
self.submit,
|
||||
@@ -491,9 +493,9 @@ pub struct InputState {
|
||||
focused: bool,
|
||||
focus_handle: gpui::FocusHandle,
|
||||
/// The key-down handler, bound to this entity with `cx.listener`.
|
||||
key_down_handler: Option<KeyDownHandler>,
|
||||
key_down_handler: KeyDownHandler,
|
||||
/// The mouse-down handler, bound to this entity with `cx.listener`.
|
||||
mouse_down_handler: Option<MouseDownHandler>,
|
||||
mouse_down_handler: MouseDownHandler,
|
||||
/// Whether a drag-select is in progress.
|
||||
dragging: bool,
|
||||
/// Patch history. `redo_stack` holds undone entries awaiting redo.
|
||||
@@ -520,9 +522,9 @@ pub struct InputState {
|
||||
/// Bumped on every edit.
|
||||
caret_epoch: usize,
|
||||
/// When the current caret epoch started.
|
||||
caret_epoch_started: Option<Instant>,
|
||||
caret_epoch_started: Instant,
|
||||
/// The shaped line and element bounds, recorded during each paint.
|
||||
layout: Rc<RefCell<Option<InputLayout>>>,
|
||||
layout: Option<InputLayout>,
|
||||
}
|
||||
|
||||
impl EventEmitter<InputStateEvent> for InputState {}
|
||||
@@ -536,16 +538,14 @@ impl InputState {
|
||||
marked_range: None,
|
||||
focused: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
key_down_handler: Some(Rc::new(cx.listener(
|
||||
|state, event: &KeyDownEvent, window, cx| {
|
||||
state.handle_key_down(event, window, cx);
|
||||
},
|
||||
))),
|
||||
mouse_down_handler: Some(Rc::new(cx.listener(
|
||||
key_down_handler: Rc::new(cx.listener(|state, event: &KeyDownEvent, window, cx| {
|
||||
state.handle_key_down(event, window, cx);
|
||||
})),
|
||||
mouse_down_handler: Rc::new(cx.listener(
|
||||
|state, event: &MouseDownEvent, window, cx| {
|
||||
state.handle_mouse_down(event, window, cx);
|
||||
},
|
||||
))),
|
||||
)),
|
||||
dragging: false,
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
@@ -559,8 +559,8 @@ impl InputState {
|
||||
scroll_offset: point(Pixels::ZERO, Pixels::ZERO),
|
||||
scroll_to_caret_pending: true,
|
||||
caret_epoch: 0,
|
||||
caret_epoch_started: Some(Instant::now()),
|
||||
layout: Rc::new(RefCell::new(None)),
|
||||
caret_epoch_started: Instant::now(),
|
||||
layout: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,12 +696,7 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which keystroke submits a multiline field.
|
||||
pub fn submit_on(&self) -> SubmitOn {
|
||||
self.submit_on
|
||||
}
|
||||
|
||||
/// Whether the painted line masks the buffer.
|
||||
// Whether the painted line masks the buffer.
|
||||
fn masked(&self) -> bool {
|
||||
self.password && !self.revealed
|
||||
}
|
||||
@@ -711,32 +706,43 @@ impl InputState {
|
||||
self.redo_stack.clear();
|
||||
|
||||
let now = Instant::now();
|
||||
if let Some(last) = self.undo_stack.last_mut()
|
||||
&& now.duration_since(last.timestamp) < self.group_interval
|
||||
{
|
||||
if let Some(last) = self.undo_stack.last_mut() {
|
||||
let last_end = last.range.start + last.new_len;
|
||||
|
||||
// Typing that continues right after the previous edit.
|
||||
if range.start == last_end {
|
||||
last.new_len += new_len;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
if self.marked_range.is_some() {
|
||||
// A pre-edit step overlapping the previous entry's region.
|
||||
if range.start >= last.range.start {
|
||||
last.new_len += new_len;
|
||||
let replaced = range.len();
|
||||
last.new_len -= replaced;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A deletion of text that sat right before the previous edit.
|
||||
if range.end == last.range.start && new_len == 0 {
|
||||
last.range.start = range.start;
|
||||
last.old_text = old_text + &last.old_text;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
if now.duration_since(last.timestamp) < self.group_interval {
|
||||
// Typing that continues right after the previous edit.
|
||||
if range.start == last_end {
|
||||
last.new_len += new_len;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// An edit fully inside the previous edit's replacement.
|
||||
if range.start >= last.range.start && range.end <= last_end {
|
||||
last.new_len += new_len;
|
||||
last.new_len -= range.len();
|
||||
last.timestamp = now;
|
||||
return;
|
||||
// A deletion of text that sat right before the previous edit.
|
||||
if range.end == last.range.start && new_len == 0 {
|
||||
last.range.start = range.start;
|
||||
last.old_text = old_text + &last.old_text;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// An edit fully inside the previous edit's replacement.
|
||||
if range.start >= last.range.start && range.end <= last_end {
|
||||
last.new_len += new_len;
|
||||
last.new_len -= range.len();
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +775,7 @@ impl InputState {
|
||||
self.scroll_to_caret_pending = true;
|
||||
self.goal_x = None;
|
||||
self.caret_epoch += 1;
|
||||
self.caret_epoch_started = Some(Instant::now());
|
||||
self.caret_epoch_started = Instant::now();
|
||||
window.notify_text_input_changed(&self.focus_handle);
|
||||
}
|
||||
|
||||
@@ -781,20 +787,38 @@ impl InputState {
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if self.multiline {
|
||||
self.scroll_offset.y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO);
|
||||
let next_y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO);
|
||||
if next_y == self.scroll_offset.y {
|
||||
return self.multiline_scrollable();
|
||||
}
|
||||
self.scroll_offset.y = next_y;
|
||||
} else {
|
||||
self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
|
||||
let next_x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
|
||||
if next_x == self.scroll_offset.x {
|
||||
return false;
|
||||
}
|
||||
self.scroll_offset.x = next_x;
|
||||
}
|
||||
|
||||
let scrollable = self
|
||||
.layout
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|layout| layout.content_height > layout.visible_height);
|
||||
|
||||
cx.notify();
|
||||
window.notify_text_input_changed(&self.focus_handle);
|
||||
self.multiline && scrollable
|
||||
}
|
||||
|
||||
/// Whether a multiline field's content overflows vertically.
|
||||
fn multiline_scrollable(&self) -> bool {
|
||||
self.multiline
|
||||
&& self
|
||||
.layout
|
||||
.as_ref()
|
||||
.is_some_and(|layout| layout.content_height > layout.visible_height)
|
||||
}
|
||||
|
||||
/// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint.
|
||||
fn sync_focus(&mut self, focused: bool, cx: &mut Context<Self>) {
|
||||
if self.focused == focused {
|
||||
@@ -802,11 +826,9 @@ impl InputState {
|
||||
}
|
||||
self.focused = focused;
|
||||
|
||||
// A fresh focus starts a fresh blink epoch, so the caret shows solid
|
||||
// for one interval before blinking resumes.
|
||||
if focused {
|
||||
self.caret_epoch += 1;
|
||||
self.caret_epoch_started = Some(Instant::now());
|
||||
self.caret_epoch_started = Instant::now();
|
||||
}
|
||||
|
||||
cx.emit(if focused {
|
||||
@@ -817,11 +839,8 @@ impl InputState {
|
||||
}
|
||||
|
||||
/// Whether the caret should paint.
|
||||
pub fn caret_visible(&mut self, focused: bool) -> bool {
|
||||
let elapsed = self
|
||||
.caret_epoch_started
|
||||
.map(|started| started.elapsed())
|
||||
.unwrap_or_default();
|
||||
pub fn caret_visible(&self, focused: bool) -> bool {
|
||||
let elapsed = self.caret_epoch_started.elapsed();
|
||||
self.caret_blink_visible_at(focused, elapsed)
|
||||
}
|
||||
|
||||
@@ -901,15 +920,21 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.cursor = to.clamp(0, self.text.len());
|
||||
if !extend {
|
||||
self.anchor = self.cursor;
|
||||
let cursor = to.clamp(0, self.text.len());
|
||||
let anchor = if extend { self.anchor } else { cursor };
|
||||
if cursor == self.cursor && anchor == self.anchor {
|
||||
return;
|
||||
}
|
||||
self.cursor = cursor;
|
||||
self.anchor = anchor;
|
||||
self.notify(window, cx);
|
||||
}
|
||||
|
||||
/// Selects `range`.
|
||||
fn select_range(&mut self, range: Range<usize>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if range.start == self.anchor && range.end == self.cursor {
|
||||
return;
|
||||
}
|
||||
self.anchor = range.start;
|
||||
self.cursor = range.end;
|
||||
self.notify(window, cx);
|
||||
@@ -1085,7 +1110,7 @@ impl InputState {
|
||||
);
|
||||
|
||||
let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO);
|
||||
*self.layout.borrow_mut() = Some(InputLayout {
|
||||
self.layout = Some(InputLayout {
|
||||
lines: text.lines,
|
||||
rows,
|
||||
bounds,
|
||||
@@ -1107,8 +1132,7 @@ impl InputState {
|
||||
return;
|
||||
}
|
||||
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1138,8 +1162,7 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1155,17 +1178,16 @@ impl InputState {
|
||||
let mut line_top = Pixels::ZERO;
|
||||
for entry in &layout.lines {
|
||||
let line_origin = point(origin.x, origin.y + line_top);
|
||||
entry
|
||||
.line
|
||||
.paint(
|
||||
line_origin,
|
||||
layout.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.ok();
|
||||
if let Err(err) = entry.line.paint(
|
||||
line_origin,
|
||||
layout.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
eprintln!("painting input text failed: {err}");
|
||||
}
|
||||
line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32;
|
||||
}
|
||||
});
|
||||
@@ -1179,8 +1201,7 @@ impl InputState {
|
||||
color: gpui::Hsla,
|
||||
window: &mut Window,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1192,7 +1213,7 @@ impl InputState {
|
||||
let caret_top = origin.y + layout.rows[row_ix].y + (layout.line_height - font_size) / 2.0;
|
||||
let caret = Bounds::from_corners(
|
||||
point(origin.x + caret_x, caret_top),
|
||||
point(origin.x + caret_x + px(1.5), caret_top + font_size),
|
||||
point(origin.x + caret_x + CARET_WIDTH, caret_top + font_size),
|
||||
);
|
||||
|
||||
window.paint_quad(fill(caret, color));
|
||||
@@ -1242,8 +1263,7 @@ impl InputState {
|
||||
|
||||
/// The text index at a window point, using the last painted layout.
|
||||
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
real_index(&layout.mask_map, layout.index_at(position))
|
||||
@@ -1256,7 +1276,11 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.cursor = self.index_at(position);
|
||||
let cursor = self.index_at(position);
|
||||
if cursor == self.cursor {
|
||||
return;
|
||||
}
|
||||
self.cursor = cursor;
|
||||
self.notify(window, cx);
|
||||
}
|
||||
|
||||
@@ -1268,14 +1292,13 @@ impl InputState {
|
||||
color: gpui::Hsla,
|
||||
window: &mut Window,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let start = display_index(&layout.mask_map, range.start);
|
||||
let end = display_index(&layout.mask_map, range.end);
|
||||
let thickness = px(1.0);
|
||||
let thickness = UNDERLINE_WIDTH;
|
||||
|
||||
for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) {
|
||||
let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness;
|
||||
@@ -1329,7 +1352,7 @@ impl InputState {
|
||||
|
||||
if event.click_count >= 3 {
|
||||
// Triple-click selects the clicked row, which is all of it when single-line.
|
||||
let row_range = self.layout.borrow().as_ref().and_then(|layout| {
|
||||
let row_range = self.layout.as_ref().and_then(|layout| {
|
||||
let display = display_index(&layout.mask_map, index);
|
||||
layout.row_range_at(display).map(|range| {
|
||||
real_index(&layout.mask_map, range.start)
|
||||
@@ -1462,15 +1485,16 @@ impl InputState {
|
||||
}
|
||||
|
||||
let target = {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let display = display_index(&layout.mask_map, self.cursor);
|
||||
|
||||
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
|
||||
return;
|
||||
};
|
||||
let target = row_ix as isize + delta;
|
||||
|
||||
let (display, goal_x) = if target < 0 {
|
||||
(0, None)
|
||||
} else if target as usize >= layout.rows.len() {
|
||||
@@ -1479,6 +1503,7 @@ impl InputState {
|
||||
let goal_x = self.goal_x.unwrap_or(caret_x);
|
||||
(layout.index_at_row_x(target as usize, goal_x), Some(goal_x))
|
||||
};
|
||||
|
||||
Some((real_index(&layout.mask_map, display), goal_x))
|
||||
};
|
||||
|
||||
@@ -1748,8 +1773,7 @@ impl EntityInputHandler for InputState {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let layout = self.layout.borrow();
|
||||
let layout = layout.as_ref()?;
|
||||
let layout = self.layout.as_ref()?;
|
||||
|
||||
let start = display_index(
|
||||
&layout.mask_map,
|
||||
@@ -1797,9 +1821,10 @@ impl EntityInputHandler for InputState {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
let layout = self.layout.borrow();
|
||||
let layout = layout.as_ref()?;
|
||||
Some(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()))
|
||||
let layout = self.layout.as_ref()?;
|
||||
let index =
|
||||
real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len());
|
||||
Some(utf8_range_to_utf16(&self.text, index..index).start)
|
||||
}
|
||||
|
||||
fn text_length_utf16(
|
||||
@@ -2049,7 +2074,11 @@ impl RenderOnce for Input {
|
||||
canvas
|
||||
.with_animation(
|
||||
animation_id,
|
||||
Animation::new(CARET_BLINK_INTERVAL * 2).repeat(),
|
||||
// The blink needs four repaints per period,
|
||||
// not the display's refresh rate.
|
||||
Animation::new(CARET_BLINK_INTERVAL * 2)
|
||||
.repeat()
|
||||
.with_max_fps(4.0),
|
||||
|canvas, _| canvas,
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -2172,23 +2201,13 @@ impl RenderOnce for Input {
|
||||
.when(!self.disabled, |input| {
|
||||
input
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let mouse_down_handler = self
|
||||
.state
|
||||
.read(cx)
|
||||
.mouse_down_handler
|
||||
.clone()
|
||||
.expect("input state always carries its mouse-down handler");
|
||||
let mouse_down_handler = self.state.read(cx).mouse_down_handler.clone();
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
mouse_down_handler(event, window, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down({
|
||||
let key_down_handler = self
|
||||
.state
|
||||
.read(cx)
|
||||
.key_down_handler
|
||||
.clone()
|
||||
.expect("input state always carries its key handler");
|
||||
let key_down_handler = self.state.read(cx).key_down_handler.clone();
|
||||
move |event: &KeyDownEvent, window, cx| {
|
||||
key_down_handler(event, window, cx);
|
||||
}
|
||||
@@ -2516,7 +2535,9 @@ fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
|
||||
|
||||
/// Converts a UTF-8 byte range to a UTF-16 range.
|
||||
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
|
||||
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
|
||||
let start = text[..range.start].encode_utf16().count();
|
||||
let end = start + text[range.start..range.end].encode_utf16().count();
|
||||
start..end
|
||||
}
|
||||
|
||||
/// Converts a UTF-16 range to a UTF-8 byte range.
|
||||
|
||||
Reference in New Issue
Block a user