Text selection (#43)
* Refactor, clarify and improve text layout * Introduce meticulous cursors for text galleys * Rename "row" to "line" We now have "paragraphs" separated by \n, which are word-wrapped onto one or more rows. * Fix some edge cases for the cursor movement * Add modifier keys and implement moving cursors one word at a time * Remove unused cursor_blink_hz * Galley: Return Rect when asking for cursor position * Implement text selection * fix: text selection when mouse goes out of the TextEdit area * Support Cmd+A ^W ^U ^K and shift-click * Create `mod util` * Implement undo for TextEdit * Move focus between text fields with tab and shift-tab * Update CHANGELOG.md
This commit is contained in:
commit
c57fba41ab
27 changed files with 2043 additions and 530 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -8,14 +8,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
|
|
||||||
* You can now check if a `TextEdit` lost keyboard focus with `response.lost_kb_focus`.
|
* `TextEdit` improvements:
|
||||||
* Added `ui.text_edit_singleline` and `ui.text_edit_multiline`.
|
* Much improved text editing, with better navigation and selection.
|
||||||
|
* Move focus between `TextEdit` widgets with tab and shift-tab.
|
||||||
|
* Undo edtis in a `TextEdit`.
|
||||||
|
* You can now check if a `TextEdit` lost keyboard focus with `response.lost_kb_focus`.
|
||||||
|
* Added `ui.text_edit_singleline` and `ui.text_edit_multiline`.
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
|
|
||||||
* Pressing enter in a single-line `TextEdit` will now surrender keyboard focus for it.
|
* Pressing enter in a single-line `TextEdit` will now surrender keyboard focus for it.
|
||||||
* You must now be explicit when creating a `TextEdit` if you want it to be singeline or multiline.
|
* You must now be explicit when creating a `TextEdit` if you want it to be singeline or multiline.
|
||||||
* Improved automatic `Id` generation, making `Id` clashes less likely.
|
* Improved automatic `Id` generation, making `Id` clashes less likely.
|
||||||
|
* Egui now requires modifier key state from the integration
|
||||||
|
* Added, renamed and removed some keys in the `Key` enum.
|
||||||
|
|
||||||
### Fixed 🐛
|
### Fixed 🐛
|
||||||
|
|
||||||
|
|
66
TODO.md
66
TODO.md
|
@ -4,8 +4,8 @@ TODO-list for the Egui project. If you looking for something to do, look here.
|
||||||
|
|
||||||
## Top priority
|
## Top priority
|
||||||
|
|
||||||
* Text input: text selection etc
|
* Egui-web copy-paste
|
||||||
* Refactor graphics layers and areas so one don't have to register LayerId:s.
|
* Egui-web fetch
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
|
@ -14,13 +14,6 @@ TODO-list for the Egui project. If you looking for something to do, look here.
|
||||||
* [ ] Tooltip widget: Something that looks like this: (?) :that shows text on hover.
|
* [ ] Tooltip widget: Something that looks like this: (?) :that shows text on hover.
|
||||||
* [ ] ui.info_button().on_hover_text("More info here");
|
* [ ] ui.info_button().on_hover_text("More info here");
|
||||||
* [ ] Allow adding multiple tooltips to the same widget, showing them all one after the other.
|
* [ ] Allow adding multiple tooltips to the same widget, showing them all one after the other.
|
||||||
* [ ] Text input
|
|
||||||
* [x] Input
|
|
||||||
* [x] Text focus
|
|
||||||
* [x] Cursor movement
|
|
||||||
* [ ] Text selection
|
|
||||||
* [ ] Clipboard copy/paste
|
|
||||||
* [ ] Move focus with tab
|
|
||||||
* [ ] Vertical slider
|
* [ ] Vertical slider
|
||||||
* [/] Color picker
|
* [/] Color picker
|
||||||
* [x] linear rgb <-> sRGB
|
* [x] linear rgb <-> sRGB
|
||||||
|
@ -32,26 +25,19 @@ TODO-list for the Egui project. If you looking for something to do, look here.
|
||||||
* [ ] Additive blending aware color picker
|
* [ ] Additive blending aware color picker
|
||||||
* [ ] Premultiplied alpha is a bit of a pain in the ass. Maybe rethink this a bit.
|
* [ ] Premultiplied alpha is a bit of a pain in the ass. Maybe rethink this a bit.
|
||||||
* [ ] Hue wheel
|
* [ ] Hue wheel
|
||||||
* Containers
|
|
||||||
* [ ] Scroll areas
|
|
||||||
* [x] Vertical scrolling
|
|
||||||
* [x] Scroll-wheel input
|
|
||||||
* [x] Drag background to scroll
|
|
||||||
* [x] Kinetic scrolling
|
|
||||||
* [ ] Horizontal scrolling
|
|
||||||
* Input
|
* Input
|
||||||
* [x] Distinguish between clicks and drags
|
* [x] Distinguish between clicks and drags
|
||||||
* [x] Double-click
|
* [x] Double-click
|
||||||
* [x] Text
|
* [x] Text
|
||||||
|
* [x] Modifier keys
|
||||||
* [ ] Support all mouse buttons
|
* [ ] Support all mouse buttons
|
||||||
* [ ] Distinguish between touch input and mouse input
|
* [ ] Distinguish between touch input and mouse input
|
||||||
* [ ] Get modifier keys
|
|
||||||
* [ ] Keyboard shortcuts
|
|
||||||
* [ ] Copy, paste, undo, ...
|
|
||||||
* Text
|
* Text
|
||||||
* [/] Unicode
|
* [/] Unicode
|
||||||
* [x] Shared mutable expanding texture map
|
* [x] Shared mutable expanding texture map
|
||||||
* [ ] Text editing of unicode
|
* [/] Text editing of unicode (needs more testing)
|
||||||
|
* [ ] Font with some more unicode characters
|
||||||
|
* [ ] Emoji support (great for things like ▶️⏸⏹⚠︎)
|
||||||
* [ ] Change text style/color and continue in same layout
|
* [ ] Change text style/color and continue in same layout
|
||||||
* Menu bar (File, Edit, etc)
|
* Menu bar (File, Edit, etc)
|
||||||
* [ ] Sub-menus
|
* [ ] Sub-menus
|
||||||
|
@ -90,26 +76,14 @@ TODO-list for the Egui project. If you looking for something to do, look here.
|
||||||
* [ ] Ask Egui if an event requires repainting
|
* [ ] Ask Egui if an event requires repainting
|
||||||
* [ ] Only repaint when mouse is over a Egui window (or is pressed and there is an active widget)
|
* [ ] Only repaint when mouse is over a Egui window (or is pressed and there is an active widget)
|
||||||
|
|
||||||
## Backends
|
## Integrations
|
||||||
|
|
||||||
* [ ] Extract egui_app as egui_backend
|
|
||||||
|
|
||||||
* egui_glium
|
|
||||||
* egui_web
|
|
||||||
* [ ] async HTTP requests
|
|
||||||
* [ ] egui_bitmap: slow reference rasterizer for tests
|
|
||||||
* Port https://github.com/emilk/imgui_software_renderer
|
|
||||||
* Less important: fast rasterizer for embedded 🤷♀️
|
|
||||||
* [ ] egui_terminal (think ncurses)
|
|
||||||
* [ ] replace `round_to_pixel` with `round_to_X` where user can select X to be e.g. width of a letter
|
|
||||||
* [ ] egui_svg: No idea what this would be for :)
|
|
||||||
|
|
||||||
### egui_web
|
### egui_web
|
||||||
|
|
||||||
* [x] Scroll input
|
* [x] Scroll input
|
||||||
* [x] Change to resize cursor on hover
|
* [x] Change to resize cursor on hover
|
||||||
* [x] Port most code to Rust
|
* [x] Port most code to Rust
|
||||||
* [x] Read url fragment and redirect to a subpage (e.g. different examples apps)]
|
* [x] Read url fragment and redirect to a subpage (e.g. different examples apps)]
|
||||||
|
* [ ] Copy/paste support
|
||||||
* [ ] Async HTTP requests
|
* [ ] Async HTTP requests
|
||||||
* [ ] Fix WebGL colors/blending (try EXT_sRGB)
|
* [ ] Fix WebGL colors/blending (try EXT_sRGB)
|
||||||
* [ ] Embeddability
|
* [ ] Embeddability
|
||||||
|
@ -120,6 +94,17 @@ TODO-list for the Egui project. If you looking for something to do, look here.
|
||||||
* Different Egui instances, same app
|
* Different Egui instances, same app
|
||||||
* Allows very nice web integration
|
* Allows very nice web integration
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* [ ] Extract egui::app as own library (egui_framework ?)
|
||||||
|
* [ ] egui_bitmap: slow reference rasterizer for tests
|
||||||
|
* Port https://github.com/emilk/imgui_software_renderer
|
||||||
|
* Less important: fast rasterizer for embedded 🤷♀️
|
||||||
|
* [ ] egui_terminal (think ncurses)
|
||||||
|
* [ ] replace `round_to_pixel` with `round_to_X` where user can select X to be e.g. width of a letter
|
||||||
|
* [ ] egui_svg: No idea what this would be for :)
|
||||||
|
|
||||||
|
|
||||||
## Modularity
|
## Modularity
|
||||||
|
|
||||||
* [x] `trait Widget` (`Label`, `Slider`, `Checkbox`, ...)
|
* [x] `trait Widget` (`Label`, `Slider`, `Checkbox`, ...)
|
||||||
|
@ -162,13 +147,24 @@ Ability to do a search for any widget. The search works even for collapsed regio
|
||||||
* [x] Collapsing header region
|
* [x] Collapsing header region
|
||||||
* [x] Tooltip
|
* [x] Tooltip
|
||||||
* [x] Movable/resizable windows
|
* [x] Movable/resizable windows
|
||||||
* [x] Kinetic windows
|
|
||||||
* [x] Add support for clicking hyperlinks
|
* [x] Add support for clicking hyperlinks
|
||||||
|
* [x] Text input
|
||||||
|
* [x] Input
|
||||||
|
* [x] Text focus
|
||||||
|
* [x] Cursor movement
|
||||||
|
* [x] Text selection
|
||||||
|
* [x] Clipboard copy/paste
|
||||||
|
* [x] Move focus with tab
|
||||||
|
* [x] Text edit undo
|
||||||
* Containers
|
* Containers
|
||||||
* [x] Vertical slider
|
* [x] Vertical slider
|
||||||
* [x] Resize any side and corner on windows
|
* [x] Resize any side and corner on windows
|
||||||
* [x] Fix autoshrink
|
* [x] Fix autoshrink
|
||||||
* [x] Automatic positioning of new windows
|
* [x] Automatic positioning of new windows
|
||||||
|
* [x] Vertical scroll areas
|
||||||
|
* [x] Scroll-wheel input
|
||||||
|
* [x] Drag background to scroll
|
||||||
|
* [x] Kinetic scrolling
|
||||||
* Simple animations
|
* Simple animations
|
||||||
* Clip rects
|
* Clip rects
|
||||||
* [x] Separate Ui::clip_rect from Ui::rect
|
* [x] Separate Ui::clip_rect from Ui::rect
|
||||||
|
|
|
@ -592,7 +592,7 @@ fn paint_frame_interaction(
|
||||||
|
|
||||||
struct TitleBar {
|
struct TitleBar {
|
||||||
title_label: Label,
|
title_label: Label,
|
||||||
title_galley: font::Galley,
|
title_galley: Galley,
|
||||||
title_rect: Rect,
|
title_rect: Rect,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,7 +205,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_frame_mut(&mut self, new_raw_input: RawInput) {
|
fn begin_frame_mut(&mut self, new_raw_input: RawInput) {
|
||||||
self.memory().begin_frame(&self.input);
|
self.memory().begin_frame(&self.input, &new_raw_input);
|
||||||
|
|
||||||
self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input);
|
self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input);
|
||||||
*self.available_rect.lock() = Some(self.input.screen_rect());
|
*self.available_rect.lock() = Some(self.input.screen_rect());
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{app, demos, Context, History, Ui};
|
use crate::{app, demos, util::History, Context, Ui};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! The input needed by Egui.
|
//! The input needed by Egui.
|
||||||
|
|
||||||
use crate::{math::*, History};
|
use crate::{math::*, util::History};
|
||||||
|
|
||||||
/// If mouse moves more than this, it is no longer a click (but maybe a drag)
|
/// If mouse moves more than this, it is no longer a click (but maybe a drag)
|
||||||
const MAX_CLICK_DIST: f32 = 6.0;
|
const MAX_CLICK_DIST: f32 = 6.0;
|
||||||
|
@ -33,6 +33,9 @@ pub struct RawInput {
|
||||||
/// Time in seconds. Relative to whatever. Used for animations.
|
/// Time in seconds. Relative to whatever. Used for animations.
|
||||||
pub time: f64,
|
pub time: f64,
|
||||||
|
|
||||||
|
/// Which modifier keys are down at the start of the frame?
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
/// In-order events received this frame
|
/// In-order events received this frame
|
||||||
pub events: Vec<Event>,
|
pub events: Vec<Event>,
|
||||||
}
|
}
|
||||||
|
@ -47,6 +50,7 @@ impl RawInput {
|
||||||
screen_size: self.screen_size,
|
screen_size: self.screen_size,
|
||||||
pixels_per_point: self.pixels_per_point,
|
pixels_per_point: self.pixels_per_point,
|
||||||
time: self.time,
|
time: self.time,
|
||||||
|
modifiers: self.modifiers,
|
||||||
events: std::mem::take(&mut self.events),
|
events: std::mem::take(&mut self.events),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +84,9 @@ pub struct InputState {
|
||||||
/// Should be set to the expected time between frames when painting at vsync speeds.
|
/// Should be set to the expected time between frames when painting at vsync speeds.
|
||||||
pub predicted_dt: f32,
|
pub predicted_dt: f32,
|
||||||
|
|
||||||
|
/// Which modifier keys are down at the start of the frame?
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
/// In-order events received this frame
|
/// In-order events received this frame
|
||||||
pub events: Vec<Event>,
|
pub events: Vec<Event>,
|
||||||
}
|
}
|
||||||
|
@ -151,7 +158,7 @@ impl Default for MouseInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An input event. Only covers events used by Egui.
|
/// An input event. Only covers events used by Egui.
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Copy,
|
Copy,
|
||||||
Cut,
|
Cut,
|
||||||
|
@ -161,33 +168,51 @@ pub enum Event {
|
||||||
Key {
|
Key {
|
||||||
key: Key,
|
key: Key,
|
||||||
pressed: bool,
|
pressed: bool,
|
||||||
|
modifiers: Modifiers,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keyboard key name. Only covers keys used by Egui.
|
/// State of the modifier keys. These must be fed to Egui.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct Modifiers {
|
||||||
|
/// Either of the alt keys are down (option ⌥ on Mac)
|
||||||
|
pub alt: bool,
|
||||||
|
/// Either of the control keys are down
|
||||||
|
pub ctrl: bool,
|
||||||
|
/// Either of the shift keys are down
|
||||||
|
pub shift: bool,
|
||||||
|
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
|
||||||
|
pub mac_cmd: bool,
|
||||||
|
/// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`).
|
||||||
|
/// On Windows and Linux, set this to the same value as `ctrl`.
|
||||||
|
/// This is so that Egui can, for instance, select all text by checking for `command + A`
|
||||||
|
/// and it will work on both Mac and Windows.
|
||||||
|
pub command: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keyboard key name. Only covers keys used by Egui (mostly for text editing).
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
Alt,
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUp,
|
||||||
Backspace,
|
Backspace,
|
||||||
Control,
|
|
||||||
Delete,
|
Delete,
|
||||||
Down,
|
|
||||||
End,
|
End,
|
||||||
|
Enter,
|
||||||
Escape,
|
Escape,
|
||||||
Home,
|
Home,
|
||||||
Insert,
|
Insert,
|
||||||
Left,
|
|
||||||
/// Windows key or Mac Command key
|
|
||||||
Logo,
|
|
||||||
PageDown,
|
PageDown,
|
||||||
PageUp,
|
PageUp,
|
||||||
/// Enter/Return key
|
|
||||||
Enter,
|
|
||||||
Right,
|
|
||||||
Shift,
|
|
||||||
// Space,
|
|
||||||
Tab,
|
Tab,
|
||||||
Up,
|
|
||||||
|
A, // Used for cmd+A (select All)
|
||||||
|
K, // Used for ctrl+K (delete text after cursor)
|
||||||
|
U, // Used for ctrl+U (delete text before cursor)
|
||||||
|
W, // Used for ctrl+W (delete previous word)
|
||||||
|
Z, // Used for cmd+Z (undo)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
|
@ -203,6 +228,7 @@ impl InputState {
|
||||||
time: new.time,
|
time: new.time,
|
||||||
unstable_dt,
|
unstable_dt,
|
||||||
predicted_dt: 1.0 / 60.0, // TODO: remove this hack
|
predicted_dt: 1.0 / 60.0, // TODO: remove this hack
|
||||||
|
modifiers: new.modifiers,
|
||||||
events: new.events.clone(), // TODO: remove clone() and use raw.events
|
events: new.events.clone(), // TODO: remove clone() and use raw.events
|
||||||
raw: new,
|
raw: new,
|
||||||
}
|
}
|
||||||
|
@ -227,7 +253,8 @@ impl InputState {
|
||||||
event,
|
event,
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: true
|
pressed: true,
|
||||||
|
..
|
||||||
} if *key == desired_key
|
} if *key == desired_key
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -240,7 +267,8 @@ impl InputState {
|
||||||
event,
|
event,
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: false
|
pressed: false,
|
||||||
|
..
|
||||||
} if *key == desired_key
|
} if *key == desired_key
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -343,6 +371,7 @@ impl RawInput {
|
||||||
screen_size,
|
screen_size,
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
time,
|
time,
|
||||||
|
modifiers,
|
||||||
events,
|
events,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -357,6 +386,7 @@ impl RawInput {
|
||||||
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
|
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
|
||||||
);
|
);
|
||||||
ui.label(format!("time: {:.3} s", time));
|
ui.label(format!("time: {:.3} s", time));
|
||||||
|
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||||
ui.label(format!("events: {:?}", events))
|
ui.label(format!("events: {:?}", events))
|
||||||
.on_hover_text("key presses etc");
|
.on_hover_text("key presses etc");
|
||||||
}
|
}
|
||||||
|
@ -373,6 +403,7 @@ impl InputState {
|
||||||
time,
|
time,
|
||||||
unstable_dt,
|
unstable_dt,
|
||||||
predicted_dt,
|
predicted_dt,
|
||||||
|
modifiers,
|
||||||
events,
|
events,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -397,6 +428,7 @@ impl InputState {
|
||||||
1e3 * unstable_dt
|
1e3 * unstable_dt
|
||||||
));
|
));
|
||||||
ui.label(format!("expected dt: {:.1} ms", 1e3 * predicted_dt));
|
ui.label(format!("expected dt: {:.1} ms", 1e3 * predicted_dt));
|
||||||
|
ui.label(format!("modifiers: {:#?}", modifiers));
|
||||||
ui.label(format!("events: {:?}", events))
|
ui.label(format!("events: {:?}", events))
|
||||||
.on_hover_text("key presses etc");
|
.on_hover_text("key presses etc");
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,11 +60,9 @@
|
||||||
pub mod align;
|
pub mod align;
|
||||||
mod animation_manager;
|
mod animation_manager;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub(crate) mod cache;
|
|
||||||
pub mod containers;
|
pub mod containers;
|
||||||
mod context;
|
mod context;
|
||||||
pub mod demos;
|
pub mod demos;
|
||||||
mod history;
|
|
||||||
mod id;
|
mod id;
|
||||||
mod input;
|
mod input;
|
||||||
mod introspection;
|
mod introspection;
|
||||||
|
@ -73,12 +71,12 @@ mod layout;
|
||||||
pub mod math;
|
pub mod math;
|
||||||
mod memory;
|
mod memory;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod mutex;
|
|
||||||
pub mod paint;
|
pub mod paint;
|
||||||
mod painter;
|
mod painter;
|
||||||
mod style;
|
mod style;
|
||||||
mod types;
|
mod types;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
pub mod util;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
|
@ -86,7 +84,6 @@ pub use {
|
||||||
containers::*,
|
containers::*,
|
||||||
context::Context,
|
context::Context,
|
||||||
demos::DemoApp,
|
demos::DemoApp,
|
||||||
history::History,
|
|
||||||
id::Id,
|
id::Id,
|
||||||
input::*,
|
input::*,
|
||||||
layers::*,
|
layers::*,
|
||||||
|
@ -101,6 +98,7 @@ pub use {
|
||||||
style::Style,
|
style::Style,
|
||||||
types::*,
|
types::*,
|
||||||
ui::Ui,
|
ui::Ui,
|
||||||
|
util::mutex,
|
||||||
widgets::*,
|
widgets::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
area,
|
area, collapsing_header, menu,
|
||||||
cache::Cache,
|
|
||||||
collapsing_header, menu,
|
|
||||||
paint::color::{Hsva, Srgba},
|
paint::color::{Hsva, Srgba},
|
||||||
resize, scroll_area,
|
resize, scroll_area,
|
||||||
|
util::Cache,
|
||||||
widgets::text_edit,
|
widgets::text_edit,
|
||||||
window, Id, LayerId, Pos2, Rect,
|
window, Id, LayerId, Pos2, Rect,
|
||||||
};
|
};
|
||||||
|
@ -84,6 +83,18 @@ pub(crate) struct Interaction {
|
||||||
/// What had keyboard focus previous frame?
|
/// What had keyboard focus previous frame?
|
||||||
pub kb_focus_id_previous_frame: Option<Id>,
|
pub kb_focus_id_previous_frame: Option<Id>,
|
||||||
|
|
||||||
|
/// If set, the next widget that is interested in kb_focus will automatically get it.
|
||||||
|
/// Probably because the user pressed Tab.
|
||||||
|
pub kb_focus_give_to_next: bool,
|
||||||
|
|
||||||
|
/// The last widget interested in kb focus.
|
||||||
|
pub kb_focus_last_interested: Option<Id>,
|
||||||
|
|
||||||
|
/// Set at the beginning of the frame, set to `false` when "used".
|
||||||
|
pressed_tab: bool,
|
||||||
|
/// Set at the beginning of the frame, set to `false` when "used".
|
||||||
|
pressed_shift_tab: bool,
|
||||||
|
|
||||||
/// HACK: windows have low priority on dragging.
|
/// HACK: windows have low priority on dragging.
|
||||||
/// This is so that if you drag a slider in a window,
|
/// This is so that if you drag a slider in a window,
|
||||||
/// the slider will steal the drag away from the window.
|
/// the slider will steal the drag away from the window.
|
||||||
|
@ -105,7 +116,11 @@ impl Interaction {
|
||||||
self.click_id.is_some() || self.drag_id.is_some()
|
self.click_id.is_some() || self.drag_id.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_frame(&mut self, prev_input: &crate::input::InputState) {
|
fn begin_frame(
|
||||||
|
&mut self,
|
||||||
|
prev_input: &crate::input::InputState,
|
||||||
|
new_input: &crate::input::RawInput,
|
||||||
|
) {
|
||||||
self.kb_focus_id_previous_frame = self.kb_focus_id;
|
self.kb_focus_id_previous_frame = self.kb_focus_id;
|
||||||
self.click_interest = false;
|
self.click_interest = false;
|
||||||
self.drag_interest = false;
|
self.drag_interest = false;
|
||||||
|
@ -119,13 +134,34 @@ impl Interaction {
|
||||||
self.click_id = None;
|
self.click_id = None;
|
||||||
self.drag_id = None;
|
self.drag_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.pressed_tab = false;
|
||||||
|
self.pressed_shift_tab = false;
|
||||||
|
for event in &new_input.events {
|
||||||
|
if let crate::input::Event::Key {
|
||||||
|
key: crate::input::Key::Tab,
|
||||||
|
pressed: true,
|
||||||
|
modifiers,
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
if modifiers.shift {
|
||||||
|
self.pressed_shift_tab = true;
|
||||||
|
} else {
|
||||||
|
self.pressed_tab = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Memory {
|
impl Memory {
|
||||||
pub(crate) fn begin_frame(&mut self, prev_input: &crate::input::InputState) {
|
pub(crate) fn begin_frame(
|
||||||
|
&mut self,
|
||||||
|
prev_input: &crate::input::InputState,
|
||||||
|
new_input: &crate::input::RawInput,
|
||||||
|
) {
|
||||||
self.used_ids.clear();
|
self.used_ids.clear();
|
||||||
self.interaction.begin_frame(prev_input);
|
self.interaction.begin_frame(prev_input, new_input);
|
||||||
|
|
||||||
if !prev_input.mouse.down {
|
if !prev_input.mouse.down {
|
||||||
self.window_interaction = None;
|
self.window_interaction = None;
|
||||||
|
@ -170,6 +206,26 @@ impl Memory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register this widget as being interested in getting keyboard focus.
|
||||||
|
/// This will allow the user to select it with tab and shift-tab.
|
||||||
|
pub fn interested_in_kb_focus(&mut self, id: Id) {
|
||||||
|
if self.interaction.kb_focus_give_to_next {
|
||||||
|
self.interaction.kb_focus_id = Some(id);
|
||||||
|
self.interaction.kb_focus_give_to_next = false;
|
||||||
|
} else if self.has_kb_focus(id) {
|
||||||
|
if self.interaction.pressed_tab {
|
||||||
|
self.interaction.kb_focus_id = None;
|
||||||
|
self.interaction.kb_focus_give_to_next = true;
|
||||||
|
self.interaction.pressed_tab = false;
|
||||||
|
} else if self.interaction.pressed_shift_tab {
|
||||||
|
self.interaction.kb_focus_id = self.interaction.kb_focus_last_interested;
|
||||||
|
self.interaction.pressed_shift_tab = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.interaction.kb_focus_last_interested = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Stop editing of active `TextEdit` (if any).
|
/// Stop editing of active `TextEdit` (if any).
|
||||||
pub fn stop_text_input(&mut self) {
|
pub fn stop_text_input(&mut self) {
|
||||||
self.interaction.kb_focus_id = None;
|
self.interaction.kb_focus_id = None;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use {
|
use {
|
||||||
super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles},
|
super::{fonts::TextStyle, Fonts, Galley, Srgba, Triangles},
|
||||||
crate::{
|
crate::{
|
||||||
align::{anchor_rect, Align},
|
align::{anchor_rect, Align},
|
||||||
math::{Pos2, Rect},
|
math::{Pos2, Rect},
|
||||||
|
|
|
@ -9,145 +9,11 @@ use {
|
||||||
use crate::{
|
use crate::{
|
||||||
math::{vec2, Vec2},
|
math::{vec2, Vec2},
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
|
paint::{Galley, Row},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::texture_atlas::TextureAtlas;
|
use super::texture_atlas::TextureAtlas;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct GalleyCursor {
|
|
||||||
/// character count in whole galley
|
|
||||||
pub char_idx: usize,
|
|
||||||
/// line number
|
|
||||||
pub line: usize,
|
|
||||||
/// character count on this line
|
|
||||||
pub column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A collection of text locked into place.
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct Galley {
|
|
||||||
/// The full text
|
|
||||||
pub text: String,
|
|
||||||
|
|
||||||
/// Lines of text, from top to bottom.
|
|
||||||
/// The number of chars in all lines sum up to text.chars().count()
|
|
||||||
pub lines: Vec<Line>,
|
|
||||||
|
|
||||||
// Optimization: calculate once and reuse.
|
|
||||||
pub size: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A typeset piece of text on a single line.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Line {
|
|
||||||
/// The start of each character, probably starting at zero.
|
|
||||||
/// The last element is the end of the last character.
|
|
||||||
/// x_offsets.len() == text.chars().count() + 1
|
|
||||||
/// This is never empty.
|
|
||||||
/// Unit: points.
|
|
||||||
pub x_offsets: Vec<f32>,
|
|
||||||
|
|
||||||
/// Top of the line, offset within the Galley.
|
|
||||||
/// Unit: points.
|
|
||||||
pub y_min: f32,
|
|
||||||
|
|
||||||
/// Bottom of the line, offset within the Galley.
|
|
||||||
/// Unit: points.
|
|
||||||
pub y_max: f32,
|
|
||||||
|
|
||||||
/// If true, the last char on this line is '\n'
|
|
||||||
pub ends_with_newline: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Galley {
|
|
||||||
pub fn sanity_check(&self) {
|
|
||||||
let mut char_count = 0;
|
|
||||||
for line in &self.lines {
|
|
||||||
line.sanity_check();
|
|
||||||
char_count += line.char_count();
|
|
||||||
}
|
|
||||||
assert_eq!(char_count, self.text.chars().count());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If given a char index after the first line, the end of the last character is returned instead.
|
|
||||||
/// Returns a Vec2 rather than a Pos2 as this is an offset into the galley. *shrug*
|
|
||||||
pub fn char_start_pos(&self, char_idx: usize) -> Vec2 {
|
|
||||||
let mut char_count = 0;
|
|
||||||
for line in &self.lines {
|
|
||||||
let line_char_count = line.char_count();
|
|
||||||
if char_count <= char_idx && char_idx < char_count + line_char_count {
|
|
||||||
let line_char_offset = char_idx - char_count;
|
|
||||||
return vec2(line.x_offsets[line_char_offset], line.y_min);
|
|
||||||
}
|
|
||||||
char_count += line_char_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(last) = self.lines.last() {
|
|
||||||
vec2(last.max_x(), last.y_min)
|
|
||||||
} else {
|
|
||||||
// Empty galley
|
|
||||||
vec2(0.0, 0.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Character offset at the given position within the galley
|
|
||||||
pub fn char_at(&self, pos: Vec2) -> GalleyCursor {
|
|
||||||
let mut best_y_dist = f32::INFINITY;
|
|
||||||
let mut cursor = GalleyCursor::default();
|
|
||||||
|
|
||||||
let mut char_count = 0;
|
|
||||||
for (line_nr, line) in self.lines.iter().enumerate() {
|
|
||||||
let y_dist = (line.y_min - pos.y).abs().min((line.y_max - pos.y).abs());
|
|
||||||
if y_dist < best_y_dist {
|
|
||||||
best_y_dist = y_dist;
|
|
||||||
let mut column = line.char_at(pos.x);
|
|
||||||
if column == line.char_count() && line.ends_with_newline && column > 0 {
|
|
||||||
// handle the case where line ends with a \n and we click after it.
|
|
||||||
// We should return the position BEFORE the \n!
|
|
||||||
column -= 1;
|
|
||||||
}
|
|
||||||
cursor = GalleyCursor {
|
|
||||||
char_idx: char_count + column,
|
|
||||||
line: line_nr,
|
|
||||||
column,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
char_count += line.char_count();
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Line {
|
|
||||||
pub fn sanity_check(&self) {
|
|
||||||
assert!(!self.x_offsets.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn char_count(&self) -> usize {
|
|
||||||
assert!(!self.x_offsets.is_empty());
|
|
||||||
self.x_offsets.len() - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn min_x(&self) -> f32 {
|
|
||||||
*self.x_offsets.first().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_x(&self) -> f32 {
|
|
||||||
*self.x_offsets.last().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closest char at the desired x coordinate. returns something in the range `[0, char_count()]`
|
|
||||||
pub fn char_at(&self, desired_x: f32) -> usize {
|
|
||||||
for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() {
|
|
||||||
let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]);
|
|
||||||
if desired_x < char_center_x {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.char_count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
|
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
|
||||||
|
@ -244,12 +110,8 @@ impl Font {
|
||||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Height of one line of text. In points
|
/// Height of one row of text. In points
|
||||||
/// TODO: rename height ?
|
pub fn row_height(&self) -> f32 {
|
||||||
pub fn line_spacing(&self) -> f32 {
|
|
||||||
self.scale_in_pixels / self.pixels_per_point
|
|
||||||
}
|
|
||||||
pub fn height(&self) -> f32 {
|
|
||||||
self.scale_in_pixels / self.pixels_per_point
|
self.scale_in_pixels / self.pixels_per_point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,12 +119,8 @@ impl Font {
|
||||||
self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect)
|
self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`)
|
||||||
fn glyph_info(&self, c: char) -> GlyphInfo {
|
fn glyph_info(&self, c: char) -> GlyphInfo {
|
||||||
if c == '\n' {
|
|
||||||
// Hack: else we show '\n' as '?' (REPLACEMENT_CHAR)
|
|
||||||
return self.glyph_info(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
if let Some(glyph_info) = self.glyph_infos.read().get(&c) {
|
if let Some(glyph_info) = self.glyph_infos.read().get(&c) {
|
||||||
return *glyph_info;
|
return *glyph_info;
|
||||||
|
@ -283,22 +141,22 @@ impl Font {
|
||||||
glyph_info
|
glyph_info
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typeset the given text onto one line.
|
/// Typeset the given text onto one row.
|
||||||
/// Assumes there are no \n in the text.
|
/// Any `\n` will show up as `REPLACEMENT_CHAR` ('?').
|
||||||
/// Always returns exactly one fragment.
|
/// Always returns exactly one `Row` in the `Galley`.
|
||||||
pub fn layout_single_line(&self, text: String) -> Galley {
|
pub fn layout_single_line(&self, text: String) -> Galley {
|
||||||
let x_offsets = self.layout_single_line_fragment(&text);
|
let x_offsets = self.layout_single_row_fragment(&text);
|
||||||
let line = Line {
|
let row = Row {
|
||||||
x_offsets,
|
x_offsets,
|
||||||
y_min: 0.0,
|
y_min: 0.0,
|
||||||
y_max: self.height(),
|
y_max: self.row_height(),
|
||||||
ends_with_newline: false,
|
ends_with_newline: false,
|
||||||
};
|
};
|
||||||
let width = line.max_x();
|
let width = row.max_x();
|
||||||
let size = vec2(width, self.height());
|
let size = vec2(width, self.row_height());
|
||||||
let galley = Galley {
|
let galley = Galley {
|
||||||
text,
|
text,
|
||||||
lines: vec![line],
|
rows: vec![row],
|
||||||
size,
|
size,
|
||||||
};
|
};
|
||||||
galley.sanity_check();
|
galley.sanity_check();
|
||||||
|
@ -306,61 +164,61 @@ impl Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
|
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
|
||||||
let line_spacing = self.line_spacing();
|
let row_height = self.row_height();
|
||||||
let mut cursor_y = 0.0;
|
let mut cursor_y = 0.0;
|
||||||
let mut lines = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
let mut paragraph_start = 0;
|
let mut paragraph_start = 0;
|
||||||
|
|
||||||
while paragraph_start < text.len() {
|
while paragraph_start < text.len() {
|
||||||
let next_newline = text[paragraph_start..].find('\n');
|
let next_newline = text[paragraph_start..].find('\n');
|
||||||
let paragraph_end = next_newline
|
let paragraph_end = next_newline
|
||||||
.map(|newline| paragraph_start + newline + 1)
|
.map(|newline| paragraph_start + newline)
|
||||||
.unwrap_or_else(|| text.len());
|
.unwrap_or_else(|| text.len());
|
||||||
|
|
||||||
assert!(paragraph_start < paragraph_end);
|
assert!(paragraph_start <= paragraph_end);
|
||||||
let paragraph_text = &text[paragraph_start..paragraph_end];
|
let paragraph_text = &text[paragraph_start..paragraph_end];
|
||||||
let mut paragraph_lines =
|
let mut paragraph_rows =
|
||||||
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
|
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
|
||||||
assert!(!paragraph_lines.is_empty());
|
assert!(!paragraph_rows.is_empty());
|
||||||
|
paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some();
|
||||||
|
|
||||||
for line in &mut paragraph_lines {
|
for row in &mut paragraph_rows {
|
||||||
line.y_min += cursor_y;
|
row.y_min += cursor_y;
|
||||||
line.y_max += cursor_y;
|
row.y_max += cursor_y;
|
||||||
}
|
}
|
||||||
cursor_y = paragraph_lines.last().unwrap().y_max;
|
cursor_y = paragraph_rows.last().unwrap().y_max;
|
||||||
cursor_y += line_spacing * 0.4; // extra spacing between paragraphs. less hacky
|
cursor_y += row_height * 0.4; // Extra spacing between paragraphs. TODO: less hacky
|
||||||
|
|
||||||
lines.append(&mut paragraph_lines);
|
rows.append(&mut paragraph_rows);
|
||||||
|
|
||||||
paragraph_start = paragraph_end;
|
paragraph_start = paragraph_end + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.is_empty() || text.ends_with('\n') {
|
if text.is_empty() || text.ends_with('\n') {
|
||||||
// Add an empty last line for correct visuals etc:
|
rows.push(Row {
|
||||||
lines.push(Line {
|
|
||||||
x_offsets: vec![0.0],
|
x_offsets: vec![0.0],
|
||||||
y_min: cursor_y,
|
y_min: cursor_y,
|
||||||
y_max: cursor_y + line_spacing,
|
y_max: cursor_y + row_height,
|
||||||
ends_with_newline: text.ends_with('\n'),
|
ends_with_newline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut widest_line = 0.0;
|
let mut widest_row = 0.0;
|
||||||
for line in &lines {
|
for row in &rows {
|
||||||
widest_line = line.max_x().max(widest_line);
|
widest_row = row.max_x().max(widest_row);
|
||||||
}
|
}
|
||||||
let size = vec2(widest_line, lines.last().unwrap().y_max);
|
let size = vec2(widest_row, rows.last().unwrap().y_max);
|
||||||
|
|
||||||
let galley = Galley { text, lines, size };
|
let galley = Galley { text, rows, size };
|
||||||
galley.sanity_check();
|
galley.sanity_check();
|
||||||
galley
|
galley
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typeset the given text onto one line.
|
/// Typeset the given text onto one row.
|
||||||
/// Assumes there are no \n in the text.
|
/// Assumes there are no `\n` in the text.
|
||||||
/// Return `x_offsets`, one longer than the number of characters in the text.
|
/// Return `x_offsets`, one longer than the number of characters in the text.
|
||||||
fn layout_single_line_fragment(&self, text: &str) -> Vec<f32> {
|
fn layout_single_row_fragment(&self, text: &str) -> Vec<f32> {
|
||||||
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
|
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
|
||||||
|
|
||||||
let mut x_offsets = Vec::with_capacity(text.chars().count() + 1);
|
let mut x_offsets = Vec::with_capacity(text.chars().count() + 1);
|
||||||
|
@ -389,59 +247,69 @@ impl Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A paragraph is text with no line break character in it.
|
/// A paragraph is text with no line break character in it.
|
||||||
/// The text will be linebreaked by the given `max_width_in_points`.
|
/// The text will be wrapped by the given `max_width_in_points`.
|
||||||
pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Line> {
|
fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Row> {
|
||||||
let full_x_offsets = self.layout_single_line_fragment(text);
|
if text == "" {
|
||||||
|
return vec![Row {
|
||||||
|
x_offsets: vec![0.0],
|
||||||
|
y_min: 0.0,
|
||||||
|
y_max: self.row_height(),
|
||||||
|
ends_with_newline: false,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
let mut line_start_x = full_x_offsets[0];
|
let full_x_offsets = self.layout_single_row_fragment(text);
|
||||||
|
|
||||||
|
let mut row_start_x = full_x_offsets[0];
|
||||||
|
|
||||||
{
|
{
|
||||||
#![allow(clippy::float_cmp)]
|
#![allow(clippy::float_cmp)]
|
||||||
assert_eq!(line_start_x, 0.0);
|
assert_eq!(row_start_x, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cursor_y = 0.0;
|
let mut cursor_y = 0.0;
|
||||||
let mut line_start_idx = 0;
|
let mut row_start_idx = 0;
|
||||||
|
|
||||||
// start index of the last space. A candidate for a new line.
|
// start index of the last space. A candidate for a new row.
|
||||||
let mut last_space = None;
|
let mut last_space = None;
|
||||||
|
|
||||||
let mut out_lines = vec![];
|
let mut out_rows = vec![];
|
||||||
|
|
||||||
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
|
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
|
||||||
let line_width = x - line_start_x;
|
debug_assert!(chr != '\n');
|
||||||
|
let potential_row_width = x - row_start_x;
|
||||||
|
|
||||||
if line_width > max_width_in_points {
|
if potential_row_width > max_width_in_points {
|
||||||
if let Some(last_space_idx) = last_space {
|
if let Some(last_space_idx) = last_space {
|
||||||
let include_trailing_space = true;
|
let include_trailing_space = true;
|
||||||
let line = if include_trailing_space {
|
let row = if include_trailing_space {
|
||||||
Line {
|
Row {
|
||||||
x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1]
|
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x - line_start_x)
|
.map(|x| x - row_start_x)
|
||||||
.collect(),
|
.collect(),
|
||||||
y_min: cursor_y,
|
y_min: cursor_y,
|
||||||
y_max: cursor_y + self.height(),
|
y_max: cursor_y + self.row_height(),
|
||||||
ends_with_newline: false, // we'll fix this later
|
ends_with_newline: false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Line {
|
Row {
|
||||||
x_offsets: full_x_offsets[line_start_idx..=last_space_idx]
|
x_offsets: full_x_offsets[row_start_idx..=last_space_idx]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x - line_start_x)
|
.map(|x| x - row_start_x)
|
||||||
.collect(),
|
.collect(),
|
||||||
y_min: cursor_y,
|
y_min: cursor_y,
|
||||||
y_max: cursor_y + self.height(),
|
y_max: cursor_y + self.row_height(),
|
||||||
ends_with_newline: false, // we'll fix this later
|
ends_with_newline: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
line.sanity_check();
|
row.sanity_check();
|
||||||
out_lines.push(line);
|
out_rows.push(row);
|
||||||
|
|
||||||
line_start_idx = last_space_idx + 1;
|
row_start_idx = last_space_idx + 1;
|
||||||
line_start_x = full_x_offsets[line_start_idx];
|
row_start_x = full_x_offsets[row_start_idx];
|
||||||
last_space = None;
|
last_space = None;
|
||||||
cursor_y += self.line_spacing();
|
cursor_y += self.row_height();
|
||||||
cursor_y = self.round_to_pixel(cursor_y);
|
cursor_y = self.round_to_pixel(cursor_y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,25 +320,21 @@ impl Font {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if line_start_idx + 1 < full_x_offsets.len() {
|
if row_start_idx + 1 < full_x_offsets.len() {
|
||||||
let line = Line {
|
let row = Row {
|
||||||
x_offsets: full_x_offsets[line_start_idx..]
|
x_offsets: full_x_offsets[row_start_idx..]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x - line_start_x)
|
.map(|x| x - row_start_x)
|
||||||
.collect(),
|
.collect(),
|
||||||
y_min: cursor_y,
|
y_min: cursor_y,
|
||||||
y_max: cursor_y + self.height(),
|
y_max: cursor_y + self.row_height(),
|
||||||
ends_with_newline: false, // we'll fix this later
|
ends_with_newline: false,
|
||||||
};
|
};
|
||||||
line.sanity_check();
|
row.sanity_check();
|
||||||
out_lines.push(line);
|
out_rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.ends_with('\n') {
|
out_rows
|
||||||
out_lines.last_mut().unwrap().ends_with_newline = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
out_lines
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
879
egui/src/paint/galley.rs
Normal file
879
egui/src/paint/galley.rs
Normal file
|
@ -0,0 +1,879 @@
|
||||||
|
//! This is going to get complicated.
|
||||||
|
//!
|
||||||
|
//! To avoid confusion, we never use the word "line".
|
||||||
|
//! The `\n` character demarcates the split of text into "paragraphs".
|
||||||
|
//! Each paragraph is wrapped at some width onto one or more "rows".
|
||||||
|
//!
|
||||||
|
//! If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||||
|
//! do we prefer the next row?
|
||||||
|
//! For instance, consider this single paragraph, word wrapped:
|
||||||
|
//! ``` text
|
||||||
|
//! Hello_
|
||||||
|
//! world!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The offset `6` is both the end of the first row
|
||||||
|
//! and the start of the second row.
|
||||||
|
//! The `prefer_next_row` selects which.
|
||||||
|
|
||||||
|
use crate::math::{pos2, NumExt, Rect, Vec2};
|
||||||
|
|
||||||
|
/// Character cursor
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct CCursor {
|
||||||
|
/// Character offset (NOT byte offset!).
|
||||||
|
pub index: usize,
|
||||||
|
|
||||||
|
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||||
|
/// do we prefer the next row?
|
||||||
|
/// This is *almost* always what you want, *except* for when
|
||||||
|
/// explicitly clicking the end of a row or pressing the end key.
|
||||||
|
pub prefer_next_row: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CCursor {
|
||||||
|
pub fn new(index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
index,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two `CCursor`s are considered equal if they refer to the same character boundary,
|
||||||
|
/// even if one prefers the start of the next row.
|
||||||
|
impl PartialEq for CCursor {
|
||||||
|
fn eq(&self, other: &CCursor) -> bool {
|
||||||
|
self.index == other.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add<usize> for CCursor {
|
||||||
|
type Output = CCursor;
|
||||||
|
fn add(self, rhs: usize) -> Self::Output {
|
||||||
|
CCursor {
|
||||||
|
index: self.index.saturating_add(rhs),
|
||||||
|
prefer_next_row: self.prefer_next_row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub<usize> for CCursor {
|
||||||
|
type Output = CCursor;
|
||||||
|
fn sub(self, rhs: usize) -> Self::Output {
|
||||||
|
CCursor {
|
||||||
|
index: self.index.saturating_sub(rhs),
|
||||||
|
prefer_next_row: self.prefer_next_row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Row Cursor
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct RCursor {
|
||||||
|
/// 0 is first row, and so on.
|
||||||
|
/// Note that a single paragraph can span multiple rows.
|
||||||
|
/// (a paragraph is text separated by `\n`).
|
||||||
|
pub row: usize,
|
||||||
|
|
||||||
|
/// Character based (NOT bytes).
|
||||||
|
/// It is fine if this points to something beyond the end of the current row.
|
||||||
|
/// When moving up/down it may again be within the next row.
|
||||||
|
pub column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paragraph Cursor
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct PCursor {
|
||||||
|
/// 0 is first paragraph, and so on.
|
||||||
|
/// Note that a single paragraph can span multiple rows.
|
||||||
|
/// (a paragraph is text separated by `\n`).
|
||||||
|
pub paragraph: usize,
|
||||||
|
|
||||||
|
/// Character based (NOT bytes).
|
||||||
|
/// It is fine if this points to something beyond the end of the current paragraph.
|
||||||
|
/// When moving up/down it may again be within the next paragraph.
|
||||||
|
pub offset: usize,
|
||||||
|
|
||||||
|
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||||
|
/// do we prefer the next row?
|
||||||
|
/// This is *almost* always what you want, *except* for when
|
||||||
|
/// explicitly clicking the end of a row or pressing the end key.
|
||||||
|
pub prefer_next_row: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two `PCursor`s are considered equal if they refer to the same character boundary,
|
||||||
|
/// even if one prefers the start of the next row.
|
||||||
|
impl PartialEq for PCursor {
|
||||||
|
fn eq(&self, other: &PCursor) -> bool {
|
||||||
|
self.paragraph == other.paragraph && self.offset == other.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All different types of cursors together.
|
||||||
|
/// They all point to the same place, but in their own different ways.
|
||||||
|
/// pcursor/rcursor can also point to after the end of the paragraph/row.
|
||||||
|
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub ccursor: CCursor,
|
||||||
|
pub rcursor: RCursor,
|
||||||
|
pub pcursor: PCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of text locked into place.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Galley {
|
||||||
|
/// The full text, including any an all `\n`.
|
||||||
|
pub text: String,
|
||||||
|
|
||||||
|
/// Rows of text, from top to bottom.
|
||||||
|
/// The number of chars in all rows sum up to text.chars().count().
|
||||||
|
/// Note that each paragraph (pieces of text separated with `\n`)
|
||||||
|
/// can be split up into multiple rows.
|
||||||
|
pub rows: Vec<Row>,
|
||||||
|
|
||||||
|
// Optimization: calculated once and reused.
|
||||||
|
pub size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A typeset piece of text on a single row.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Row {
|
||||||
|
/// The start of each character, probably starting at zero.
|
||||||
|
/// The last element is the end of the last character.
|
||||||
|
/// This is never empty.
|
||||||
|
/// Unit: points.
|
||||||
|
///
|
||||||
|
/// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1`
|
||||||
|
pub x_offsets: Vec<f32>,
|
||||||
|
|
||||||
|
/// Top of the row, offset within the Galley.
|
||||||
|
/// Unit: points.
|
||||||
|
pub y_min: f32,
|
||||||
|
|
||||||
|
/// Bottom of the row, offset within the Galley.
|
||||||
|
/// Unit: points.
|
||||||
|
pub y_max: f32,
|
||||||
|
|
||||||
|
/// If true, this `Row` came from a paragraph ending with a `\n`.
|
||||||
|
/// The `\n` itself is omitted from `x_offsets`.
|
||||||
|
/// A `\n` in the input text always creates a new `Row` below it,
|
||||||
|
/// so that text that ends with `\n` has an empty `Row` last.
|
||||||
|
/// This also implies that the last `Row` in a `Galley` always has `ends_with_newline == false`.
|
||||||
|
pub ends_with_newline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Row {
|
||||||
|
pub fn sanity_check(&self) {
|
||||||
|
assert!(!self.x_offsets.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Excludes the implicit `\n` after the `Row`, if any.
|
||||||
|
pub fn char_count_excluding_newline(&self) -> usize {
|
||||||
|
assert!(!self.x_offsets.is_empty());
|
||||||
|
self.x_offsets.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Includes the implicit `\n` after the `Row`, if any.
|
||||||
|
pub fn char_count_including_newline(&self) -> usize {
|
||||||
|
self.char_count_excluding_newline() + (self.ends_with_newline as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_x(&self) -> f32 {
|
||||||
|
*self.x_offsets.first().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_x(&self) -> f32 {
|
||||||
|
*self.x_offsets.last().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> f32 {
|
||||||
|
self.y_max - self.y_min
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closest char at the desired x coordinate.
|
||||||
|
/// Returns something in the range `[0, char_count_excluding_newline()]`.
|
||||||
|
pub fn char_at(&self, desired_x: f32) -> usize {
|
||||||
|
for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() {
|
||||||
|
let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]);
|
||||||
|
if desired_x < char_center_x {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.char_count_excluding_newline()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x_offset(&self, column: usize) -> f32 {
|
||||||
|
self.x_offsets[column.min(self.x_offsets.len() - 1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Galley {
|
||||||
|
pub fn sanity_check(&self) {
|
||||||
|
let mut char_count = 0;
|
||||||
|
for row in &self.rows {
|
||||||
|
row.sanity_check();
|
||||||
|
char_count += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
assert_eq!(char_count, self.text.chars().count());
|
||||||
|
if let Some(last_row) = self.rows.last() {
|
||||||
|
debug_assert!(
|
||||||
|
!last_row.ends_with_newline,
|
||||||
|
"If the text ends with '\\n', there would be an empty row last.\n\
|
||||||
|
Galley: {:#?}",
|
||||||
|
self
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Physical positions
|
||||||
|
impl Galley {
|
||||||
|
fn end_pos(&self) -> Rect {
|
||||||
|
if let Some(row) = self.rows.last() {
|
||||||
|
let x = row.max_x();
|
||||||
|
Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max))
|
||||||
|
} else {
|
||||||
|
// Empty galley
|
||||||
|
Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a 0-width Rect.
|
||||||
|
pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect {
|
||||||
|
let mut it = PCursor::default();
|
||||||
|
|
||||||
|
for row in &self.rows {
|
||||||
|
if it.paragraph == pcursor.paragraph {
|
||||||
|
// Right paragraph, but is it the right row in the paragraph?
|
||||||
|
|
||||||
|
if it.offset <= pcursor.offset
|
||||||
|
&& (pcursor.offset <= it.offset + row.char_count_excluding_newline()
|
||||||
|
|| row.ends_with_newline)
|
||||||
|
{
|
||||||
|
let column = pcursor.offset - it.offset;
|
||||||
|
|
||||||
|
let select_next_row_instead = pcursor.prefer_next_row
|
||||||
|
&& !row.ends_with_newline
|
||||||
|
&& column >= row.char_count_excluding_newline();
|
||||||
|
if !select_next_row_instead {
|
||||||
|
let x = row.x_offset(column);
|
||||||
|
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.ends_with_newline {
|
||||||
|
it.paragraph += 1;
|
||||||
|
it.offset = 0;
|
||||||
|
} else {
|
||||||
|
it.offset += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.end_pos()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a 0-width Rect.
|
||||||
|
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
||||||
|
self.pos_from_pcursor(cursor.pcursor) // The one TextEdit stores
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor at the given position within the galley
|
||||||
|
pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor {
|
||||||
|
let mut best_y_dist = f32::INFINITY;
|
||||||
|
let mut cursor = Cursor::default();
|
||||||
|
|
||||||
|
let mut ccursor_index = 0;
|
||||||
|
let mut pcursor_it = PCursor::default();
|
||||||
|
|
||||||
|
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||||
|
let y_dist = (row.y_min - pos.y).abs().min((row.y_max - pos.y).abs());
|
||||||
|
if y_dist < best_y_dist {
|
||||||
|
best_y_dist = y_dist;
|
||||||
|
let column = row.char_at(pos.x);
|
||||||
|
let prefer_next_row = column < row.char_count_excluding_newline();
|
||||||
|
cursor = Cursor {
|
||||||
|
ccursor: CCursor {
|
||||||
|
index: ccursor_index + column,
|
||||||
|
prefer_next_row,
|
||||||
|
},
|
||||||
|
rcursor: RCursor {
|
||||||
|
row: row_nr,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: pcursor_it.paragraph,
|
||||||
|
offset: pcursor_it.offset + column,
|
||||||
|
prefer_next_row,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ccursor_index += row.char_count_including_newline();
|
||||||
|
if row.ends_with_newline {
|
||||||
|
pcursor_it.paragraph += 1;
|
||||||
|
pcursor_it.offset = 0;
|
||||||
|
} else {
|
||||||
|
pcursor_it.offset += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Cursor positions
|
||||||
|
impl Galley {
|
||||||
|
/// Cursor to one-past last character.
|
||||||
|
pub fn end(&self) -> Cursor {
|
||||||
|
if self.rows.is_empty() {
|
||||||
|
return Default::default();
|
||||||
|
}
|
||||||
|
let mut ccursor = CCursor {
|
||||||
|
index: 0,
|
||||||
|
prefer_next_row: true,
|
||||||
|
};
|
||||||
|
let mut pcursor = PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row: true,
|
||||||
|
};
|
||||||
|
for row in &self.rows {
|
||||||
|
let row_char_count = row.char_count_including_newline();
|
||||||
|
ccursor.index += row_char_count;
|
||||||
|
if row.ends_with_newline {
|
||||||
|
pcursor.paragraph += 1;
|
||||||
|
pcursor.offset = 0;
|
||||||
|
} else {
|
||||||
|
pcursor.offset += row_char_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cursor {
|
||||||
|
ccursor,
|
||||||
|
rcursor: self.end_rcursor(),
|
||||||
|
pcursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_rcursor(&self) -> RCursor {
|
||||||
|
if let Some(last_row) = self.rows.last() {
|
||||||
|
debug_assert!(!last_row.ends_with_newline);
|
||||||
|
RCursor {
|
||||||
|
row: self.rows.len() - 1,
|
||||||
|
column: last_row.char_count_excluding_newline(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Cursor conversions
|
||||||
|
impl Galley {
|
||||||
|
// The returned cursor is clamped.
|
||||||
|
pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor {
|
||||||
|
let prefer_next_row = ccursor.prefer_next_row;
|
||||||
|
let mut ccursor_it = CCursor {
|
||||||
|
index: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
let mut pcursor_it = PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||||
|
let row_char_count = row.char_count_excluding_newline();
|
||||||
|
|
||||||
|
if ccursor_it.index <= ccursor.index
|
||||||
|
&& ccursor.index <= ccursor_it.index + row_char_count
|
||||||
|
{
|
||||||
|
let column = ccursor.index - ccursor_it.index;
|
||||||
|
|
||||||
|
let select_next_row_instead = prefer_next_row
|
||||||
|
&& !row.ends_with_newline
|
||||||
|
&& column >= row.char_count_excluding_newline();
|
||||||
|
if !select_next_row_instead {
|
||||||
|
pcursor_it.offset += column;
|
||||||
|
return Cursor {
|
||||||
|
ccursor,
|
||||||
|
rcursor: RCursor {
|
||||||
|
row: row_nr,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
pcursor: pcursor_it,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ccursor_it.index += row.char_count_including_newline();
|
||||||
|
if row.ends_with_newline {
|
||||||
|
pcursor_it.paragraph += 1;
|
||||||
|
pcursor_it.offset = 0;
|
||||||
|
} else {
|
||||||
|
pcursor_it.offset += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug_assert_eq!(ccursor_it, self.end().ccursor);
|
||||||
|
Cursor {
|
||||||
|
ccursor: ccursor_it, // clamp
|
||||||
|
rcursor: self.end_rcursor(),
|
||||||
|
pcursor: pcursor_it,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor {
|
||||||
|
if rcursor.row >= self.rows.len() {
|
||||||
|
return self.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefer_next_row =
|
||||||
|
rcursor.column < self.rows[rcursor.row].char_count_excluding_newline();
|
||||||
|
let mut ccursor_it = CCursor {
|
||||||
|
index: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
let mut pcursor_it = PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||||
|
if row_nr == rcursor.row {
|
||||||
|
ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline());
|
||||||
|
|
||||||
|
if row.ends_with_newline {
|
||||||
|
// Allow offset to go beyond the end of the paragraph
|
||||||
|
pcursor_it.offset += rcursor.column;
|
||||||
|
} else {
|
||||||
|
pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline());
|
||||||
|
}
|
||||||
|
return Cursor {
|
||||||
|
ccursor: ccursor_it,
|
||||||
|
rcursor,
|
||||||
|
pcursor: pcursor_it,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ccursor_it.index += row.char_count_including_newline();
|
||||||
|
if row.ends_with_newline {
|
||||||
|
pcursor_it.paragraph += 1;
|
||||||
|
pcursor_it.offset = 0;
|
||||||
|
} else {
|
||||||
|
pcursor_it.offset += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cursor {
|
||||||
|
ccursor: ccursor_it,
|
||||||
|
rcursor: self.end_rcursor(),
|
||||||
|
pcursor: pcursor_it,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return identical cursor, or clamp?
|
||||||
|
pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor {
|
||||||
|
let prefer_next_row = pcursor.prefer_next_row;
|
||||||
|
let mut ccursor_it = CCursor {
|
||||||
|
index: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
let mut pcursor_it = PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||||
|
if pcursor_it.paragraph == pcursor.paragraph {
|
||||||
|
// Right paragraph, but is it the right row in the paragraph?
|
||||||
|
|
||||||
|
if pcursor_it.offset <= pcursor.offset
|
||||||
|
&& (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline()
|
||||||
|
|| row.ends_with_newline)
|
||||||
|
{
|
||||||
|
let column = pcursor.offset - pcursor_it.offset;
|
||||||
|
|
||||||
|
let select_next_row_instead = pcursor.prefer_next_row
|
||||||
|
&& !row.ends_with_newline
|
||||||
|
&& column >= row.char_count_excluding_newline();
|
||||||
|
|
||||||
|
if !select_next_row_instead {
|
||||||
|
ccursor_it.index += column.at_most(row.char_count_excluding_newline());
|
||||||
|
|
||||||
|
return Cursor {
|
||||||
|
ccursor: ccursor_it,
|
||||||
|
rcursor: RCursor {
|
||||||
|
row: row_nr,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
pcursor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ccursor_it.index += row.char_count_including_newline();
|
||||||
|
if row.ends_with_newline {
|
||||||
|
pcursor_it.paragraph += 1;
|
||||||
|
pcursor_it.offset = 0;
|
||||||
|
} else {
|
||||||
|
pcursor_it.offset += row.char_count_including_newline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cursor {
|
||||||
|
ccursor: ccursor_it,
|
||||||
|
rcursor: self.end_rcursor(),
|
||||||
|
pcursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Cursor positions
|
||||||
|
impl Galley {
|
||||||
|
pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
if cursor.ccursor.index == 0 {
|
||||||
|
Default::default()
|
||||||
|
} else {
|
||||||
|
let ccursor = CCursor {
|
||||||
|
index: cursor.ccursor.index,
|
||||||
|
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
|
||||||
|
};
|
||||||
|
self.from_ccursor(ccursor - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
let ccursor = CCursor {
|
||||||
|
index: cursor.ccursor.index,
|
||||||
|
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
|
||||||
|
};
|
||||||
|
self.from_ccursor(ccursor + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
if cursor.rcursor.row == 0 {
|
||||||
|
Cursor::default()
|
||||||
|
} else {
|
||||||
|
let new_row = cursor.rcursor.row - 1;
|
||||||
|
|
||||||
|
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
|
||||||
|
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
|
||||||
|
|
||||||
|
let new_rcursor = if cursor_is_beyond_end_of_current_row {
|
||||||
|
// keep same column
|
||||||
|
RCursor {
|
||||||
|
row: new_row,
|
||||||
|
column: cursor.rcursor.column,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// keep same X coord
|
||||||
|
let x = self.pos_from_cursor(cursor).center().x;
|
||||||
|
let column = if x > self.rows[new_row].max_x() {
|
||||||
|
// beyond the end of this row - keep same colum
|
||||||
|
cursor.rcursor.column
|
||||||
|
} else {
|
||||||
|
self.rows[new_row].char_at(x)
|
||||||
|
};
|
||||||
|
RCursor {
|
||||||
|
row: new_row,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.from_rcursor(new_rcursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
if cursor.rcursor.row + 1 < self.rows.len() {
|
||||||
|
let new_row = cursor.rcursor.row + 1;
|
||||||
|
|
||||||
|
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
|
||||||
|
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
|
||||||
|
|
||||||
|
let new_rcursor = if cursor_is_beyond_end_of_current_row {
|
||||||
|
// keep same column
|
||||||
|
RCursor {
|
||||||
|
row: new_row,
|
||||||
|
column: cursor.rcursor.column,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// keep same X coord
|
||||||
|
let x = self.pos_from_cursor(cursor).center().x;
|
||||||
|
let column = if x > self.rows[new_row].max_x() {
|
||||||
|
// beyond the end of the next row - keep same column
|
||||||
|
cursor.rcursor.column
|
||||||
|
} else {
|
||||||
|
self.rows[new_row].char_at(x)
|
||||||
|
};
|
||||||
|
RCursor {
|
||||||
|
row: new_row,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.from_rcursor(new_rcursor)
|
||||||
|
} else {
|
||||||
|
self.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
self.from_rcursor(RCursor {
|
||||||
|
row: cursor.rcursor.row,
|
||||||
|
column: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor {
|
||||||
|
self.from_rcursor(RCursor {
|
||||||
|
row: cursor.rcursor.row,
|
||||||
|
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_text_layout() {
|
||||||
|
impl PartialEq for Cursor {
|
||||||
|
fn eq(&self, other: &Cursor) -> bool {
|
||||||
|
(self.ccursor, self.rcursor, self.pcursor)
|
||||||
|
== (other.ccursor, other.rcursor, other.pcursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::mutex::Mutex;
|
||||||
|
use crate::paint::{font::Font, *};
|
||||||
|
|
||||||
|
let pixels_per_point = 1.0;
|
||||||
|
let typeface_data = include_bytes!("../../fonts/ProggyClean.ttf");
|
||||||
|
let atlas = TextureAtlas::new(512, 16);
|
||||||
|
let atlas = std::sync::Arc::new(Mutex::new(atlas));
|
||||||
|
let font = Font::new(atlas, typeface_data, 13.0, pixels_per_point);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 1);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[0].x_offsets, vec![0.0]);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("\n".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 2);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("\n\n".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 3);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[2].x_offsets, vec![0.0]);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline(" ".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 1);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("One row!".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 1);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("First row!\n".to_owned(), 1024.0);
|
||||||
|
assert_eq!(galley.rows.len(), 2);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
||||||
|
|
||||||
|
let galley = font.layout_multiline("line\nbreak".to_owned(), 10.0);
|
||||||
|
assert_eq!(galley.rows.len(), 2);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
|
||||||
|
// Test wrapping:
|
||||||
|
let galley = font.layout_multiline("word wrap".to_owned(), 10.0);
|
||||||
|
assert_eq!(galley.rows.len(), 2);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
|
||||||
|
{
|
||||||
|
// Test wrapping:
|
||||||
|
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
|
||||||
|
assert_eq!(galley.rows.len(), 4);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[0].char_count_excluding_newline(), "word ".len());
|
||||||
|
assert_eq!(galley.rows[0].char_count_including_newline(), "word ".len());
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[1].char_count_excluding_newline(), "wrap.".len());
|
||||||
|
assert_eq!(
|
||||||
|
galley.rows[1].char_count_including_newline(),
|
||||||
|
"wrap.\n".len()
|
||||||
|
);
|
||||||
|
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[3].ends_with_newline, false);
|
||||||
|
|
||||||
|
let cursor = Cursor::default();
|
||||||
|
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||||
|
|
||||||
|
let cursor = galley.end();
|
||||||
|
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||||
|
assert_eq!(
|
||||||
|
cursor,
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(25),
|
||||||
|
rcursor: RCursor { row: 3, column: 10 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 1,
|
||||||
|
offset: 14,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = galley.from_ccursor(CCursor::new(1));
|
||||||
|
assert_eq!(cursor.rcursor, RCursor { row: 0, column: 1 });
|
||||||
|
assert_eq!(
|
||||||
|
cursor.pcursor,
|
||||||
|
PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 1,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||||
|
|
||||||
|
let cursor = galley.from_pcursor(PCursor {
|
||||||
|
paragraph: 1,
|
||||||
|
offset: 2,
|
||||||
|
prefer_next_row: false,
|
||||||
|
});
|
||||||
|
assert_eq!(cursor.rcursor, RCursor { row: 2, column: 2 });
|
||||||
|
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||||
|
|
||||||
|
let cursor = galley.from_pcursor(PCursor {
|
||||||
|
paragraph: 1,
|
||||||
|
offset: 6,
|
||||||
|
prefer_next_row: false,
|
||||||
|
});
|
||||||
|
assert_eq!(cursor.rcursor, RCursor { row: 3, column: 2 });
|
||||||
|
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||||
|
|
||||||
|
// On the border between two rows within the same paragraph:
|
||||||
|
let cursor = galley.from_rcursor(RCursor { row: 0, column: 5 });
|
||||||
|
assert_eq!(
|
||||||
|
cursor,
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(5),
|
||||||
|
rcursor: RCursor { row: 0, column: 5 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 5,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
|
||||||
|
let cursor = galley.from_rcursor(RCursor { row: 1, column: 0 });
|
||||||
|
assert_eq!(
|
||||||
|
cursor,
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(5),
|
||||||
|
rcursor: RCursor { row: 1, column: 0 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 5,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Test cursor movement:
|
||||||
|
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
|
||||||
|
assert_eq!(galley.rows.len(), 4);
|
||||||
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||||
|
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||||
|
assert_eq!(galley.rows[3].ends_with_newline, false);
|
||||||
|
|
||||||
|
let cursor = Cursor::default();
|
||||||
|
|
||||||
|
assert_eq!(galley.cursor_up_one_row(&cursor), cursor);
|
||||||
|
assert_eq!(galley.cursor_begin_of_row(&cursor), cursor);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
galley.cursor_end_of_row(&cursor),
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(5),
|
||||||
|
rcursor: RCursor { row: 0, column: 5 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 5,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
galley.cursor_down_one_row(&cursor),
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(5),
|
||||||
|
rcursor: RCursor { row: 1, column: 0 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 0,
|
||||||
|
offset: 5,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = Cursor::default();
|
||||||
|
assert_eq!(
|
||||||
|
galley.cursor_down_one_row(&galley.cursor_down_one_row(&cursor)),
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(11),
|
||||||
|
rcursor: RCursor { row: 2, column: 0 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 1,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = galley.end();
|
||||||
|
assert_eq!(galley.cursor_down_one_row(&cursor), cursor);
|
||||||
|
|
||||||
|
let cursor = galley.end();
|
||||||
|
assert!(galley.cursor_up_one_row(&galley.end()) != cursor);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
galley.cursor_up_one_row(&galley.end()),
|
||||||
|
Cursor {
|
||||||
|
ccursor: CCursor::new(15),
|
||||||
|
rcursor: RCursor { row: 2, column: 10 },
|
||||||
|
pcursor: PCursor {
|
||||||
|
paragraph: 1,
|
||||||
|
offset: 4,
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ pub mod color;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod font;
|
pub mod font;
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
|
mod galley;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod tessellator;
|
pub mod tessellator;
|
||||||
mod texture_atlas;
|
mod texture_atlas;
|
||||||
|
@ -14,9 +15,10 @@ pub use {
|
||||||
color::{Rgba, Srgba},
|
color::{Rgba, Srgba},
|
||||||
command::{PaintCmd, Stroke},
|
command::{PaintCmd, Stroke},
|
||||||
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
||||||
|
galley::*,
|
||||||
stats::PaintStats,
|
stats::PaintStats,
|
||||||
tessellator::{
|
tessellator::{
|
||||||
PaintJob, PaintJobs, TesselationOptions, TextureId, Triangles, Vertex, WHITE_UV,
|
PaintJob, PaintJobs, TesselationOptions, TextureId, Triangles, Vertex, WHITE_UV,
|
||||||
},
|
},
|
||||||
texture_atlas::Texture,
|
texture_atlas::{Texture, TextureAtlas},
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,8 +66,8 @@ impl AllocInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_galley(galley: &font::Galley) -> Self {
|
pub fn from_galley(galley: &Galley) -> Self {
|
||||||
Self::from_slice(galley.text.as_bytes()) + Self::from_slice(&galley.lines)
|
Self::from_slice(galley.text.as_bytes()) + Self::from_slice(&galley.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_triangles(triangles: &Triangles) -> Self {
|
pub fn from_triangles(triangles: &Triangles) -> Self {
|
||||||
|
|
|
@ -787,13 +787,13 @@ fn tessellate_paint_command(
|
||||||
|
|
||||||
let text_offset = vec2(0.0, 1.0); // Eye-balled for buttons. TODO: why is this needed?
|
let text_offset = vec2(0.0, 1.0); // Eye-balled for buttons. TODO: why is this needed?
|
||||||
|
|
||||||
let clip_rect = clip_rect.expand(2.0); // Some fudge to handle letter slightly larger than expected.
|
let clip_rect = clip_rect.expand(2.0); // Some fudge to handle letters that are slightly larger than expected.
|
||||||
|
|
||||||
let font = &fonts[text_style];
|
let font = &fonts[text_style];
|
||||||
let mut chars = galley.text.chars();
|
let mut chars = galley.text.chars();
|
||||||
for line in &galley.lines {
|
for line in &galley.rows {
|
||||||
let line_min_y = pos.y + line.y_min + text_offset.x;
|
let line_min_y = pos.y + line.y_min + text_offset.x;
|
||||||
let line_max_y = line_min_y + font.height();
|
let line_max_y = line_min_y + font.row_height();
|
||||||
let is_line_visible =
|
let is_line_visible =
|
||||||
line_max_y >= clip_rect.min.y && line_min_y <= clip_rect.max.y;
|
line_max_y >= clip_rect.min.y && line_min_y <= clip_rect.max.y;
|
||||||
|
|
||||||
|
@ -820,6 +820,10 @@ fn tessellate_paint_command(
|
||||||
out.add_rect_with_uv(pos, uv, color);
|
out.add_rect_with_uv(pos, uv, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if line.ends_with_newline {
|
||||||
|
let newline = chars.next().unwrap();
|
||||||
|
debug_assert_eq!(newline, '\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(chars.next(), None);
|
assert_eq!(chars.next(), None);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
color,
|
color,
|
||||||
layers::PaintCmdIdx,
|
layers::PaintCmdIdx,
|
||||||
math::{Pos2, Rect, Vec2},
|
math::{Pos2, Rect, Vec2},
|
||||||
paint::{font, Fonts, PaintCmd, Stroke, TextStyle},
|
paint::{Fonts, Galley, PaintCmd, Stroke, TextStyle},
|
||||||
Context, LayerId, Srgba,
|
Context, LayerId, Srgba,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ impl Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint text that has already been layed out in a `Galley`.
|
/// Paint text that has already been layed out in a `Galley`.
|
||||||
pub fn galley(&self, pos: Pos2, galley: font::Galley, text_style: TextStyle, color: Srgba) {
|
pub fn galley(&self, pos: Pos2, galley: Galley, text_style: TextStyle, color: Srgba) {
|
||||||
self.add(PaintCmd::Text {
|
self.add(PaintCmd::Text {
|
||||||
pos,
|
pos,
|
||||||
galley,
|
galley,
|
||||||
|
|
|
@ -131,8 +131,6 @@ pub struct Visuals {
|
||||||
|
|
||||||
pub resize_corner_size: f32,
|
pub resize_corner_size: f32,
|
||||||
|
|
||||||
/// Blink text cursor by this frequency. If 0, always show the cursor.
|
|
||||||
pub cursor_blink_hz: f32,
|
|
||||||
pub text_cursor_width: f32,
|
pub text_cursor_width: f32,
|
||||||
|
|
||||||
/// Allow child widgets to be just on the border and still have a stroke with some thickness
|
/// Allow child widgets to be just on the border and still have a stroke with some thickness
|
||||||
|
@ -260,7 +258,6 @@ impl Default for Visuals {
|
||||||
dark_bg_color: Srgba::black_alpha(140),
|
dark_bg_color: Srgba::black_alpha(140),
|
||||||
window_corner_radius: 10.0,
|
window_corner_radius: 10.0,
|
||||||
resize_corner_size: 12.0,
|
resize_corner_size: 12.0,
|
||||||
cursor_blink_hz: 0.0, // 1.0 looks good
|
|
||||||
text_cursor_width: 2.0,
|
text_cursor_width: 2.0,
|
||||||
clip_rect_margin: 3.0,
|
clip_rect_margin: 3.0,
|
||||||
debug_widget_rects: false,
|
debug_widget_rects: false,
|
||||||
|
@ -444,7 +441,6 @@ impl Visuals {
|
||||||
dark_bg_color,
|
dark_bg_color,
|
||||||
window_corner_radius,
|
window_corner_radius,
|
||||||
resize_corner_size,
|
resize_corner_size,
|
||||||
cursor_blink_hz,
|
|
||||||
text_cursor_width,
|
text_cursor_width,
|
||||||
clip_rect_margin,
|
clip_rect_margin,
|
||||||
debug_widget_rects,
|
debug_widget_rects,
|
||||||
|
@ -455,7 +451,6 @@ impl Visuals {
|
||||||
ui_color(ui, dark_bg_color, "dark_bg_color");
|
ui_color(ui, dark_bg_color, "dark_bg_color");
|
||||||
ui.add(Slider::f32(window_corner_radius, 0.0..=20.0).text("window_corner_radius"));
|
ui.add(Slider::f32(window_corner_radius, 0.0..=20.0).text("window_corner_radius"));
|
||||||
ui.add(Slider::f32(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
ui.add(Slider::f32(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
||||||
ui.add(Slider::f32(cursor_blink_hz, 0.0..=4.0).text("cursor_blink_hz"));
|
|
||||||
ui.add(Slider::f32(text_cursor_width, 0.0..=2.0).text("text_cursor_width"));
|
ui.add(Slider::f32(text_cursor_width, 0.0..=2.0).text("text_cursor_width"));
|
||||||
ui.add(Slider::f32(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
ui.add(Slider::f32(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
||||||
|
|
||||||
|
|
9
egui/src/util/mod.rs
Normal file
9
egui/src/util/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//! Tools used by Egui, but that doesn't depend on anything in Egui.
|
||||||
|
|
||||||
|
pub(crate) mod cache;
|
||||||
|
mod history;
|
||||||
|
pub mod mutex;
|
||||||
|
pub mod undoer;
|
||||||
|
|
||||||
|
pub(crate) use cache::Cache;
|
||||||
|
pub use history::History;
|
172
egui/src/util/undoer.rs
Normal file
172
egui/src/util/undoer.rs
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct Settings {
|
||||||
|
/// Maximum number of undos.
|
||||||
|
/// If your state is resource intensive, you should keep this low.
|
||||||
|
///
|
||||||
|
/// Default: `100`
|
||||||
|
pub max_undos: usize,
|
||||||
|
|
||||||
|
/// When that state hasn't changed for this many seconds,
|
||||||
|
/// create a new undo point (if one is needed).
|
||||||
|
///
|
||||||
|
/// Default value: `1.0` seconds.
|
||||||
|
pub stable_time: f32,
|
||||||
|
|
||||||
|
/// If the state is changing so often that we never get to `stable_time`,
|
||||||
|
/// then still create a save point every `auto_save_interval` seconds,
|
||||||
|
/// so we have something to undo to.
|
||||||
|
///
|
||||||
|
/// Default value: `30` seconds.
|
||||||
|
pub auto_save_interval: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_undos: 100,
|
||||||
|
stable_time: 1.0,
|
||||||
|
auto_save_interval: 30.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automatic undo system.
|
||||||
|
///
|
||||||
|
/// Every frame you feed it the most recent state.
|
||||||
|
/// The `Undoer` compares it with the latest undo point
|
||||||
|
/// and if there is a change it may create a new undo point.
|
||||||
|
///
|
||||||
|
/// `Undoer` follows two simple rules:
|
||||||
|
///
|
||||||
|
/// 1) If the state has changed since the latest undo point, but has
|
||||||
|
/// remained stable for `stable_time` seconds, an new undo point is created.
|
||||||
|
/// 2) If the state does not stabilize within `auto_save_interval` seconds, an undo point is created.
|
||||||
|
///
|
||||||
|
/// Rule 1) will make sure an undo point is not created until you _stop_ dragging that slider.
|
||||||
|
/// Rule 2) will make sure that you will get some undo points even if you are constantly changing the state.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct Undoer<State> {
|
||||||
|
settings: Settings,
|
||||||
|
|
||||||
|
/// New undoes are added to the back.
|
||||||
|
/// Two adjacent undo points are never equal.
|
||||||
|
/// The latest undo point may (often) be the current state.
|
||||||
|
undos: VecDeque<State>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
flux: Option<Flux<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> std::fmt::Debug for Undoer<State> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self { undos, .. } = self;
|
||||||
|
f.debug_struct("Undoer")
|
||||||
|
.field("undo count", &undos.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents how the current state is changing
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Flux<State> {
|
||||||
|
start_time: f64,
|
||||||
|
latest_change_time: f64,
|
||||||
|
latest_state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> Undoer<State>
|
||||||
|
where
|
||||||
|
State: Clone + PartialEq,
|
||||||
|
{
|
||||||
|
/// Do we have an undo point different from the given state?
|
||||||
|
pub fn has_undo(&self, current_state: &State) -> bool {
|
||||||
|
match self.undos.len() {
|
||||||
|
0 => false,
|
||||||
|
1 => self.undos.back() != Some(current_state),
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if the state is currently changing
|
||||||
|
pub fn is_in_flux(&self) -> bool {
|
||||||
|
self.flux.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self, current_state: &State) -> Option<&State> {
|
||||||
|
if self.has_undo(current_state) {
|
||||||
|
self.flux = None;
|
||||||
|
|
||||||
|
if self.undos.back() == Some(current_state) {
|
||||||
|
self.undos.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we keep the undo point intact.
|
||||||
|
self.undos.back()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an undo point if, and only if, there has been a change since the latest undo point.
|
||||||
|
///
|
||||||
|
/// * `time`: current time in seconds.
|
||||||
|
pub fn add_undo(&mut self, current_state: &State) {
|
||||||
|
if self.undos.back() != Some(current_state) {
|
||||||
|
self.undos.push_back(current_state.clone());
|
||||||
|
}
|
||||||
|
while self.undos.len() > self.settings.max_undos {
|
||||||
|
self.undos.pop_front();
|
||||||
|
}
|
||||||
|
self.flux = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this as often as you want (e.g. every frame)
|
||||||
|
/// and `Undoer` will determine if a new undo point should be created.
|
||||||
|
///
|
||||||
|
/// * `current_time`: current time in seconds.
|
||||||
|
pub fn feed_state(&mut self, current_time: f64, current_state: &State) {
|
||||||
|
match self.undos.back() {
|
||||||
|
None => {
|
||||||
|
// First time feed_state is called.
|
||||||
|
// always create an undo point:
|
||||||
|
self.add_undo(current_state);
|
||||||
|
}
|
||||||
|
Some(latest_undo) => {
|
||||||
|
if latest_undo == current_state {
|
||||||
|
self.flux = None;
|
||||||
|
} else {
|
||||||
|
match self.flux.as_mut() {
|
||||||
|
None => {
|
||||||
|
self.flux = Some(Flux {
|
||||||
|
start_time: current_time,
|
||||||
|
latest_change_time: current_time,
|
||||||
|
latest_state: current_state.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(flux) => {
|
||||||
|
if &flux.latest_state == current_state {
|
||||||
|
let time_since_latest_change =
|
||||||
|
(current_time - flux.latest_change_time) as f32;
|
||||||
|
if time_since_latest_change >= self.settings.stable_time {
|
||||||
|
self.add_undo(current_state);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let time_since_flux_start = (current_time - flux.start_time) as f32;
|
||||||
|
if time_since_flux_start >= self.settings.auto_save_interval {
|
||||||
|
self.add_undo(current_state);
|
||||||
|
} else {
|
||||||
|
flux.latest_change_time = current_time;
|
||||||
|
flux.latest_state = current_state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ impl Label {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout(&self, ui: &Ui) -> font::Galley {
|
pub fn layout(&self, ui: &Ui) -> Galley {
|
||||||
let max_width = ui.available().width();
|
let max_width = ui.available().width();
|
||||||
// Prevent word-wrapping after a single letter, and other silly shit:
|
// Prevent word-wrapping after a single letter, and other silly shit:
|
||||||
// TODO: general "don't force labels and similar to wrap so early"
|
// TODO: general "don't force labels and similar to wrap so early"
|
||||||
|
@ -86,7 +86,7 @@ impl Label {
|
||||||
self.layout_width(ui, max_width)
|
self.layout_width(ui, max_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_width(&self, ui: &Ui, max_width: f32) -> font::Galley {
|
pub fn layout_width(&self, ui: &Ui, max_width: f32) -> Galley {
|
||||||
let text_style = self.text_style_or_default(ui.style());
|
let text_style = self.text_style_or_default(ui.style());
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
if self.multiline {
|
if self.multiline {
|
||||||
|
@ -98,7 +98,7 @@ impl Label {
|
||||||
|
|
||||||
pub fn font_height(&self, fonts: &Fonts, style: &Style) -> f32 {
|
pub fn font_height(&self, fonts: &Fonts, style: &Style) -> f32 {
|
||||||
let text_style = self.text_style_or_default(style);
|
let text_style = self.text_style_or_default(style);
|
||||||
fonts[text_style].height()
|
fonts[text_style].row_height()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should return a LabelLayout which has a paint method.
|
// TODO: this should return a LabelLayout which has a paint method.
|
||||||
|
@ -109,7 +109,7 @@ impl Label {
|
||||||
// TODO: a paint method for painting anywhere in a ui.
|
// TODO: a paint method for painting anywhere in a ui.
|
||||||
// This should be the easiest method of putting text anywhere.
|
// This should be the easiest method of putting text anywhere.
|
||||||
|
|
||||||
pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) {
|
pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: Galley) {
|
||||||
let text_style = self.text_style_or_default(ui.style());
|
let text_style = self.text_style_or_default(ui.style());
|
||||||
let text_color = self
|
let text_color = self
|
||||||
.text_color
|
.text_color
|
||||||
|
@ -222,7 +222,7 @@ impl Widget for Hyperlink {
|
||||||
|
|
||||||
if response.hovered {
|
if response.hovered {
|
||||||
// Underline:
|
// Underline:
|
||||||
for line in &galley.lines {
|
for line in &galley.rows {
|
||||||
let pos = response.rect.min;
|
let pos = response.rect.min;
|
||||||
let y = pos.y + line.y_max;
|
let y = pos.y + line.y_max;
|
||||||
let y = ui.painter().round_to_pixel(y);
|
let y = ui.painter().round_to_pixel(y);
|
||||||
|
|
|
@ -367,7 +367,7 @@ impl<'a> Widget for Slider<'a> {
|
||||||
let text_style = TextStyle::Button;
|
let text_style = TextStyle::Button;
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
let height = font
|
let height = font
|
||||||
.line_spacing()
|
.row_height()
|
||||||
.at_least(ui.style().spacing.interact_size.y);
|
.at_least(ui.style().spacing.interact_size.y);
|
||||||
|
|
||||||
if self.text.is_some() {
|
if self.text.is_some() {
|
||||||
|
|
|
@ -1,11 +1,99 @@
|
||||||
use crate::{paint::*, *};
|
use crate::{paint::*, util::undoer::Undoer, *};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
pub(crate) struct State {
|
||||||
|
cursorp: Option<CursorPair>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
undoer: Undoer<(CCursorPair, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub(crate) struct State {
|
struct CursorPair {
|
||||||
/// Character based, NOT bytes.
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
/// TODO: store as line + row
|
/// When moving with e.g. shift+arrows, this is what moves.
|
||||||
pub cursor: Option<usize>,
|
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||||
|
pub primary: Cursor,
|
||||||
|
|
||||||
|
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||||
|
/// This part of the cursor does not move when shift is down.
|
||||||
|
pub secondary: Cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorPair {
|
||||||
|
fn one(cursor: Cursor) -> Self {
|
||||||
|
Self {
|
||||||
|
primary: cursor,
|
||||||
|
secondary: cursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two(min: Cursor, max: Cursor) -> Self {
|
||||||
|
Self {
|
||||||
|
primary: max,
|
||||||
|
secondary: min,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_ccursorp(&self) -> CCursorPair {
|
||||||
|
CCursorPair {
|
||||||
|
primary: self.primary.ccursor,
|
||||||
|
secondary: self.secondary.ccursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.primary.ccursor == self.secondary.ccursor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If there is a selection, None is returned.
|
||||||
|
/// If the two ends is the same, that is returned.
|
||||||
|
fn single(&self) -> Option<Cursor> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Some(self.primary)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_is_first(&self) -> bool {
|
||||||
|
let p = self.primary.ccursor;
|
||||||
|
let s = self.secondary.ccursor;
|
||||||
|
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sorted(&self) -> [Cursor; 2] {
|
||||||
|
if self.primary_is_first() {
|
||||||
|
[self.primary, self.secondary]
|
||||||
|
} else {
|
||||||
|
[self.secondary, self.primary]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
struct CCursorPair {
|
||||||
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
|
/// When moving with e.g. shift+arrows, this is what moves.
|
||||||
|
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||||
|
pub primary: CCursor,
|
||||||
|
|
||||||
|
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||||
|
/// This part of the cursor does not move when shift is down.
|
||||||
|
pub secondary: CCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CCursorPair {
|
||||||
|
fn one(ccursor: CCursor) -> Self {
|
||||||
|
Self {
|
||||||
|
primary: ccursor,
|
||||||
|
secondary: ccursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A text region that the user can edit the contents of.
|
/// A text region that the user can edit the contents of.
|
||||||
|
@ -142,7 +230,7 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
|
|
||||||
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
let line_spacing = font.line_spacing();
|
let line_spacing = font.row_height();
|
||||||
let available_width = ui.available().width();
|
let available_width = ui.available().width();
|
||||||
let mut galley = if multiline {
|
let mut galley = if multiline {
|
||||||
font.layout_multiline(text.clone(), available_width)
|
font.layout_multiline(text.clone(), available_width)
|
||||||
|
@ -162,17 +250,59 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
} else {
|
} else {
|
||||||
Sense::nothing()
|
Sense::nothing()
|
||||||
};
|
};
|
||||||
let response = ui.interact(rect, id, sense); // TODO: implement drag-select
|
let response = ui.interact(rect, id, sense);
|
||||||
|
|
||||||
if response.clicked && enabled {
|
if enabled {
|
||||||
ui.memory().request_kb_focus(id);
|
ui.memory().interested_in_kb_focus(id);
|
||||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
|
||||||
state.cursor = Some(galley.char_at(mouse_pos - response.rect.min).char_idx);
|
|
||||||
}
|
}
|
||||||
} else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) {
|
|
||||||
|
if enabled {
|
||||||
|
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||||
|
// TODO: triple-click to select whole paragraph
|
||||||
|
// TODO: drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||||
|
|
||||||
|
let cursor_at_mouse = galley.cursor_from_pos(mouse_pos - response.rect.min);
|
||||||
|
|
||||||
|
if response.hovered {
|
||||||
|
// preview:
|
||||||
|
let end_color = Rgba::new(0.1, 0.6, 1.0, 1.0).multiply(0.5).into(); // TODO: from style
|
||||||
|
paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse, end_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.hovered && response.double_clicked {
|
||||||
|
// Select word:
|
||||||
|
let center = cursor_at_mouse;
|
||||||
|
let primary =
|
||||||
|
galley.from_ccursor(ccursor_next_word(&galley.text, center.ccursor));
|
||||||
|
state.cursorp = Some(CursorPair {
|
||||||
|
secondary: galley
|
||||||
|
.from_ccursor(ccursor_previous_word(&galley.text, primary.ccursor)),
|
||||||
|
primary,
|
||||||
|
});
|
||||||
|
} else if response.hovered && ui.input().mouse.pressed {
|
||||||
|
ui.memory().request_kb_focus(id);
|
||||||
|
if ui.input().modifiers.shift {
|
||||||
|
if let Some(cursorp) = &mut state.cursorp {
|
||||||
|
cursorp.primary = cursor_at_mouse;
|
||||||
|
} else {
|
||||||
|
state.cursorp = Some(CursorPair::one(cursor_at_mouse));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.cursorp = Some(CursorPair::one(cursor_at_mouse));
|
||||||
|
}
|
||||||
|
} else if ui.input().mouse.down && response.active {
|
||||||
|
if let Some(cursorp) = &mut state.cursorp {
|
||||||
|
cursorp.primary = cursor_at_mouse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.input().mouse.pressed && !response.hovered {
|
||||||
// User clicked somewhere else
|
// User clicked somewhere else
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
}
|
}
|
||||||
|
@ -182,27 +312,70 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_kb_focus(id) && enabled {
|
if ui.memory().has_kb_focus(id) && enabled {
|
||||||
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
|
let mut cursorp = state
|
||||||
cursor = clamp(cursor, 0..=text.chars().count());
|
.cursorp
|
||||||
|
.map(|cursorp| {
|
||||||
|
// We only keep the PCursor (paragraph number, and character offset within that paragraph).
|
||||||
|
// This is so what if we resize the `TextEdit` region, and text wrapping changes,
|
||||||
|
// we keep the same byte character offset from the beginning of the text,
|
||||||
|
// even though the number of rows changes
|
||||||
|
// (each paragraph can be several rows, due to word wrapping).
|
||||||
|
// The column (character offset) should be able to extend beyond the last word so that we can
|
||||||
|
// go down and still end up on the same column when we return.
|
||||||
|
CursorPair {
|
||||||
|
primary: galley.from_pcursor(cursorp.primary.pcursor),
|
||||||
|
secondary: galley.from_pcursor(cursorp.secondary.pcursor),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| CursorPair::one(galley.end()));
|
||||||
|
|
||||||
|
// We feed state to the undoer both before and after handling input
|
||||||
|
// so that the undoer creates automatic saves even when there are no events for a while.
|
||||||
|
state
|
||||||
|
.undoer
|
||||||
|
.feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone()));
|
||||||
|
|
||||||
for event in &ui.input().events {
|
for event in &ui.input().events {
|
||||||
match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy | Event::Cut => {
|
Event::Copy => {
|
||||||
// TODO: cut
|
if cursorp.is_empty() {
|
||||||
ui.ctx().output().copied_text = text.clone();
|
ui.ctx().output().copied_text = text.clone();
|
||||||
|
} else {
|
||||||
|
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Event::Cut => {
|
||||||
|
if cursorp.is_empty() {
|
||||||
|
ui.ctx().output().copied_text = std::mem::take(text);
|
||||||
|
Some(CCursorPair::default())
|
||||||
|
} else {
|
||||||
|
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
|
||||||
|
Some(CCursorPair::one(delete_selected(text, &cursorp)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Event::Text(text_to_insert) => {
|
Event::Text(text_to_insert) => {
|
||||||
// newlines are handled by `Key::Enter`.
|
// Newlines are handled by `Key::Enter`.
|
||||||
if text_to_insert != "\n" && text_to_insert != "\r" {
|
if !text_to_insert.is_empty()
|
||||||
insert_text(&mut cursor, text, text_to_insert);
|
&& text_to_insert != "\n"
|
||||||
|
&& text_to_insert != "\r"
|
||||||
|
{
|
||||||
|
let mut ccursor = delete_selected(text, &cursorp);
|
||||||
|
insert_text(&mut ccursor, text, text_to_insert);
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: Key::Enter,
|
key: Key::Enter,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
if multiline {
|
if multiline {
|
||||||
insert_text(&mut cursor, text, "\n");
|
let mut ccursor = delete_selected(text, &cursorp);
|
||||||
|
insert_text(&mut ccursor, text, "\n");
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
// Common to end input with enter
|
// Common to end input with enter
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
|
@ -212,19 +385,39 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: Key::Escape,
|
key: Key::Escape,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Event::Key { key, pressed: true } => {
|
|
||||||
on_key_press(&mut cursor, text, *key);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.cursor = Some(cursor);
|
|
||||||
|
|
||||||
// layout again to avoid frame delay:
|
Event::Key {
|
||||||
|
key: Key::Z,
|
||||||
|
pressed: true,
|
||||||
|
modifiers,
|
||||||
|
} if modifiers.command && !modifiers.shift => {
|
||||||
|
// TODO: redo
|
||||||
|
if let Some((undo_ccursorp, undo_txt)) =
|
||||||
|
state.undoer.undo(&(cursorp.as_ccursorp(), text.clone()))
|
||||||
|
{
|
||||||
|
*text = undo_txt.clone();
|
||||||
|
Some(*undo_ccursorp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::Key {
|
||||||
|
key,
|
||||||
|
pressed: true,
|
||||||
|
modifiers,
|
||||||
|
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
|
||||||
|
|
||||||
|
Event::Key { .. } => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(new_ccursorp) = did_mutate_text {
|
||||||
|
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
galley = if multiline {
|
galley = if multiline {
|
||||||
font.layout_multiline(text.clone(), available_width)
|
font.layout_multiline(text.clone(), available_width)
|
||||||
|
@ -232,15 +425,24 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
font.layout_single_line(text.clone())
|
font.layout_single_line(text.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
// dbg!(&galley);
|
// Set cursorp using new galley:
|
||||||
|
cursorp = CursorPair {
|
||||||
|
primary: galley.from_ccursor(new_ccursorp.primary),
|
||||||
|
secondary: galley.from_ccursor(new_ccursorp.secondary),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.cursorp = Some(cursorp);
|
||||||
|
|
||||||
|
state
|
||||||
|
.undoer
|
||||||
|
.feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
let bg_rect = response.rect.expand(2.0); // breathing room for content
|
let bg_rect = response.rect.expand(2.0); // breathing room for content
|
||||||
painter.add(PaintCmd::Rect {
|
ui.painter().add(PaintCmd::Rect {
|
||||||
rect: bg_rect,
|
rect: bg_rect,
|
||||||
corner_radius: visuals.corner_radius,
|
corner_radius: visuals.corner_radius,
|
||||||
fill: ui.style().visuals.dark_bg_color,
|
fill: ui.style().visuals.dark_bg_color,
|
||||||
|
@ -250,29 +452,22 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_kb_focus(id) {
|
if ui.memory().has_kb_focus(id) {
|
||||||
let cursor_blink_hz = ui.style().visuals.cursor_blink_hz;
|
if let Some(cursorp) = state.cursorp {
|
||||||
let show_cursor = if 0.0 < cursor_blink_hz {
|
// TODO: color from Style
|
||||||
ui.ctx().request_repaint(); // TODO: only when cursor blinks on or off
|
let selection_color = Rgba::new(0.0, 0.5, 1.0, 0.0).multiply(0.15).into(); // additive!
|
||||||
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0
|
let end_color = Rgba::new(0.3, 0.6, 1.0, 1.0).into();
|
||||||
} else {
|
paint_cursor_selection(ui, response.rect.min, &galley, &cursorp, selection_color);
|
||||||
true
|
paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary, end_color);
|
||||||
};
|
|
||||||
|
|
||||||
if show_cursor {
|
|
||||||
if let Some(cursor) = state.cursor {
|
|
||||||
let cursor_pos = response.rect.min + galley.char_start_pos(cursor);
|
|
||||||
painter.line_segment(
|
|
||||||
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
|
|
||||||
(ui.style().visuals.text_cursor_width, color::WHITE),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or(ui.style().visuals.override_text_color)
|
.or(ui.style().visuals.override_text_color)
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
||||||
painter.galley(response.rect.min, galley, text_style, text_color);
|
.unwrap_or_else(|| ui.style().visuals.widgets.inactive.text_color());
|
||||||
|
ui.painter()
|
||||||
|
.galley(response.rect.min, galley, text_style, text_color);
|
||||||
|
|
||||||
ui.memory().text_edit.insert(id, state);
|
ui.memory().text_edit.insert(id, state);
|
||||||
|
|
||||||
Response {
|
Response {
|
||||||
|
@ -282,114 +477,363 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_text(cursor: &mut usize, text: &mut String, text_to_insert: &str) {
|
// ----------------------------------------------------------------------------
|
||||||
// eprintln!("insert_text {:?}", text_to_insert);
|
|
||||||
|
|
||||||
|
fn paint_cursor_selection(
|
||||||
|
ui: &mut Ui,
|
||||||
|
pos: Pos2,
|
||||||
|
galley: &Galley,
|
||||||
|
cursorp: &CursorPair,
|
||||||
|
color: Srgba,
|
||||||
|
) {
|
||||||
|
if cursorp.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let [min, max] = cursorp.sorted();
|
||||||
|
let min = min.rcursor;
|
||||||
|
let max = max.rcursor;
|
||||||
|
|
||||||
|
for ri in min.row..=max.row {
|
||||||
|
let row = &galley.rows[ri];
|
||||||
|
let left = if ri == min.row {
|
||||||
|
row.x_offset(min.column)
|
||||||
|
} else {
|
||||||
|
row.min_x()
|
||||||
|
};
|
||||||
|
let right = if ri == max.row {
|
||||||
|
row.x_offset(max.column)
|
||||||
|
} else {
|
||||||
|
let newline_size = if row.ends_with_newline {
|
||||||
|
row.height() / 2.0 // visualize that we select the newline
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
row.max_x() + newline_size
|
||||||
|
};
|
||||||
|
let rect = Rect::from_min_max(pos + vec2(left, row.y_min), pos + vec2(right, row.y_max));
|
||||||
|
ui.painter().rect_filled(rect, 0.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor, color: Srgba) {
|
||||||
|
let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
||||||
|
let cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
||||||
|
|
||||||
|
let top = cursor_pos.center_top();
|
||||||
|
let bottom = cursor_pos.center_bottom();
|
||||||
|
|
||||||
|
ui.painter()
|
||||||
|
.line_segment([top, bottom], (ui.style().visuals.text_cursor_width, color));
|
||||||
|
|
||||||
|
if false {
|
||||||
|
// Roof/floor:
|
||||||
|
let extrusion = 3.0;
|
||||||
|
let width = 1.0;
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
|
||||||
|
(width, color),
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
|
||||||
|
(width, color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn selected_str<'s>(text: &'s str, cursorp: &CursorPair) -> &'s str {
|
||||||
|
let [min, max] = cursorp.sorted();
|
||||||
|
let byte_begin = byte_index_from_char_index(text, min.ccursor.index);
|
||||||
|
let byte_end = byte_index_from_char_index(text, max.ccursor.index);
|
||||||
|
&text[byte_begin..byte_end]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
|
||||||
|
for (ci, (bi, _)) in s.char_indices().enumerate() {
|
||||||
|
if ci == char_index {
|
||||||
|
return bi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
let mut new_text = String::with_capacity(text.len() + text_to_insert.len());
|
||||||
for _ in 0..*cursor {
|
for _ in 0..ccursor.index {
|
||||||
let c = char_it.next().unwrap();
|
let c = char_it.next().unwrap();
|
||||||
new_text.push(c);
|
new_text.push(c);
|
||||||
}
|
}
|
||||||
*cursor += text_to_insert.chars().count();
|
ccursor.index += text_to_insert.chars().count();
|
||||||
new_text += text_to_insert;
|
new_text += text_to_insert;
|
||||||
new_text.extend(char_it);
|
new_text.extend(char_it);
|
||||||
*text = new_text;
|
*text = new_text;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_key_press(cursor: &mut usize, text: &mut String, key: Key) {
|
// ----------------------------------------------------------------------------
|
||||||
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
|
|
||||||
|
|
||||||
match key {
|
fn delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor {
|
||||||
Key::Backspace if *cursor > 0 => {
|
let [min, max] = cursorp.sorted();
|
||||||
*cursor -= 1;
|
delete_selected_ccursor_range(text, [min.ccursor, max.ccursor])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> CCursor {
|
||||||
|
let [min, max] = [min.index, max.index];
|
||||||
|
assert!(min <= max);
|
||||||
|
if min < max {
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
let mut new_text = String::with_capacity(text.len());
|
||||||
for _ in 0..*cursor {
|
for _ in 0..min {
|
||||||
new_text.push(char_it.next().unwrap())
|
new_text.push(char_it.next().unwrap())
|
||||||
}
|
}
|
||||||
new_text.extend(char_it.skip(1));
|
new_text.extend(char_it.skip(max - min));
|
||||||
*text = new_text;
|
*text = new_text;
|
||||||
}
|
}
|
||||||
|
CCursor {
|
||||||
|
index: min,
|
||||||
|
prefer_next_row: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor {
|
||||||
|
if ccursor.index > 0 {
|
||||||
|
let max_ccursor = ccursor;
|
||||||
|
let min_ccursor = max_ccursor - 1;
|
||||||
|
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
|
||||||
|
} else {
|
||||||
|
ccursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_next_char(text: &mut String, ccursor: CCursor) -> CCursor {
|
||||||
|
delete_selected_ccursor_range(text, [ccursor, ccursor + 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor {
|
||||||
|
let min_ccursor = ccursor_previous_word(text, max_ccursor);
|
||||||
|
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor {
|
||||||
|
let max_ccursor = ccursor_next_word(text, min_ccursor);
|
||||||
|
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_paragraph_before_cursor(
|
||||||
|
text: &mut String,
|
||||||
|
galley: &Galley,
|
||||||
|
cursorp: &CursorPair,
|
||||||
|
) -> CCursor {
|
||||||
|
let [min, max] = cursorp.sorted();
|
||||||
|
let min = galley.from_pcursor(PCursor {
|
||||||
|
paragraph: min.pcursor.paragraph,
|
||||||
|
offset: 0,
|
||||||
|
prefer_next_row: true,
|
||||||
|
});
|
||||||
|
if min.ccursor == max.ccursor {
|
||||||
|
delete_previous_char(text, min.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_selected(text, &CursorPair::two(min, max))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_paragraph_after_cursor(
|
||||||
|
text: &mut String,
|
||||||
|
galley: &Galley,
|
||||||
|
cursorp: &CursorPair,
|
||||||
|
) -> CCursor {
|
||||||
|
let [min, max] = cursorp.sorted();
|
||||||
|
let max = galley.from_pcursor(PCursor {
|
||||||
|
paragraph: max.pcursor.paragraph,
|
||||||
|
offset: usize::MAX, // end of paragraph
|
||||||
|
prefer_next_row: false,
|
||||||
|
});
|
||||||
|
if min.ccursor == max.ccursor {
|
||||||
|
delete_next_char(text, min.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_selected(text, &CursorPair::two(min, max))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
||||||
|
fn on_key_press(
|
||||||
|
cursorp: &mut CursorPair,
|
||||||
|
text: &mut String,
|
||||||
|
galley: &Galley,
|
||||||
|
key: Key,
|
||||||
|
modifiers: &Modifiers,
|
||||||
|
) -> Option<CCursorPair> {
|
||||||
|
match key {
|
||||||
|
Key::Backspace => {
|
||||||
|
let ccursor = if modifiers.mac_cmd {
|
||||||
|
delete_paragraph_before_cursor(text, galley, cursorp)
|
||||||
|
} else if let Some(cursor) = cursorp.single() {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
delete_previous_word(text, cursor.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_previous_char(text, cursor.ccursor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete_selected(text, cursorp)
|
||||||
|
};
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
}
|
||||||
Key::Delete => {
|
Key::Delete => {
|
||||||
let mut char_it = text.chars();
|
let ccursor = if modifiers.mac_cmd {
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
delete_paragraph_after_cursor(text, galley, cursorp)
|
||||||
for _ in 0..*cursor {
|
} else if let Some(cursor) = cursorp.single() {
|
||||||
new_text.push(char_it.next().unwrap())
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
delete_next_word(text, cursor.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_next_char(text, cursor.ccursor)
|
||||||
}
|
}
|
||||||
new_text.extend(char_it.skip(1));
|
} else {
|
||||||
*text = new_text;
|
delete_selected(text, cursorp)
|
||||||
|
};
|
||||||
|
let ccursor = CCursor {
|
||||||
|
prefer_next_row: true,
|
||||||
|
..ccursor
|
||||||
|
};
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
}
|
}
|
||||||
Key::Enter => {} // handled earlier
|
|
||||||
|
Key::A if modifiers.command => {
|
||||||
|
// select all
|
||||||
|
*cursorp = CursorPair::two(Cursor::default(), galley.end());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::K if modifiers.ctrl => {
|
||||||
|
let ccursor = delete_paragraph_after_cursor(text, galley, cursorp);
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::U if modifiers.ctrl => {
|
||||||
|
let ccursor = delete_paragraph_before_cursor(text, galley, cursorp);
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::W if modifiers.ctrl => {
|
||||||
|
let ccursor = if let Some(cursor) = cursorp.single() {
|
||||||
|
delete_previous_word(text, cursor.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_selected(text, cursorp)
|
||||||
|
};
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End => {
|
||||||
|
move_single_cursor(&mut cursorp.primary, galley, key, modifiers);
|
||||||
|
if !modifiers.shift {
|
||||||
|
cursorp.secondary = cursorp.primary;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
|
||||||
|
match key {
|
||||||
|
Key::ArrowLeft => {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*cursor = galley.from_ccursor(ccursor_previous_word(&galley.text, cursor.ccursor));
|
||||||
|
} else if modifiers.mac_cmd {
|
||||||
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_left_one_character(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowRight => {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*cursor = galley.from_ccursor(ccursor_next_word(&galley.text, cursor.ccursor));
|
||||||
|
} else if modifiers.mac_cmd {
|
||||||
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_right_one_character(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowUp => {
|
||||||
|
if modifiers.command {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_up_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowDown => {
|
||||||
|
if modifiers.command {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = galley.end();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_down_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Key::Home => {
|
Key::Home => {
|
||||||
// To start of paragraph:
|
if modifiers.ctrl {
|
||||||
let pos = line_col_from_char_idx(text, *cursor);
|
// windows behavior
|
||||||
*cursor = char_idx_from_line_col(text, (pos.0, 0));
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Key::End => {
|
Key::End => {
|
||||||
// To end of paragraph:
|
if modifiers.ctrl {
|
||||||
let pos = line_col_from_char_idx(text, *cursor);
|
// windows behavior
|
||||||
let line = line_from_number(text, pos.0);
|
*cursor = galley.end();
|
||||||
*cursor = char_idx_from_line_col(text, (pos.0, line.chars().count()));
|
} else {
|
||||||
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
}
|
}
|
||||||
Key::Left if *cursor > 0 => {
|
|
||||||
*cursor -= 1;
|
|
||||||
}
|
|
||||||
Key::Right => {
|
|
||||||
*cursor = (*cursor + 1).min(text.chars().count());
|
|
||||||
}
|
|
||||||
Key::Up => {
|
|
||||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
|
||||||
pos.0 = pos.0.saturating_sub(1);
|
|
||||||
*cursor = char_idx_from_line_col(text, pos);
|
|
||||||
}
|
|
||||||
Key::Down => {
|
|
||||||
let mut pos = line_col_from_char_idx(text, *cursor);
|
|
||||||
pos.0 += 1;
|
|
||||||
*cursor = char_idx_from_line_col(text, pos);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_col_from_char_idx(s: &str, char_idx: usize) -> (usize, usize) {
|
// ----------------------------------------------------------------------------
|
||||||
let mut char_count = 0;
|
|
||||||
|
|
||||||
let mut last_line_nr = 0;
|
fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
let mut last_line = s;
|
CCursor {
|
||||||
for (line_nr, line) in s.split('\n').enumerate() {
|
index: next_word_char_index(text.chars(), ccursor.index),
|
||||||
let line_width = line.chars().count();
|
prefer_next_row: false,
|
||||||
if char_idx <= char_count + line_width {
|
|
||||||
return (line_nr, char_idx - char_count);
|
|
||||||
}
|
}
|
||||||
char_count += line_width + 1;
|
|
||||||
last_line_nr = line_nr;
|
|
||||||
last_line = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// safe fallback:
|
|
||||||
(last_line_nr, last_line.chars().count())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_idx_from_line_col(s: &str, pos: (usize, usize)) -> usize {
|
fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
let mut char_count = 0;
|
let num_chars = text.chars().count();
|
||||||
for (line_nr, line) in s.split('\n').enumerate() {
|
CCursor {
|
||||||
if line_nr == pos.0 {
|
index: num_chars - next_word_char_index(text.chars().rev(), num_chars - ccursor.index),
|
||||||
return char_count + pos.1.min(line.chars().count());
|
prefer_next_row: true,
|
||||||
}
|
}
|
||||||
char_count += line.chars().count() + 1;
|
|
||||||
}
|
|
||||||
char_count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_from_number(s: &str, desired_line_number: usize) -> &str {
|
fn next_word_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||||
for (line_nr, line) in s.split('\n').enumerate() {
|
let mut it = it.skip(index);
|
||||||
if line_nr == desired_line_number {
|
if let Some(_first) = it.next() {
|
||||||
return line;
|
index += 1;
|
||||||
|
|
||||||
|
if let Some(second) = it.next() {
|
||||||
|
index += 1;
|
||||||
|
for next in it {
|
||||||
|
if is_word_char(next) != is_word_char(second) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s
|
}
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_char(c: char) -> bool {
|
||||||
|
c.is_ascii_alphanumeric() || c == '_'
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,6 @@ pub use clipboard::ClipboardContext; // TODO: remove
|
||||||
|
|
||||||
pub struct GliumInputState {
|
pub struct GliumInputState {
|
||||||
raw: egui::RawInput,
|
raw: egui::RawInput,
|
||||||
|
|
||||||
/// Command modifier key.
|
|
||||||
/// Mac command key on Mac, ctrl on Window/Linux.
|
|
||||||
cmd: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GliumInputState {
|
impl GliumInputState {
|
||||||
|
@ -33,7 +29,6 @@ impl GliumInputState {
|
||||||
pixels_per_point: Some(pixels_per_point),
|
pixels_per_point: Some(pixels_per_point),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cmd: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,40 +58,51 @@ pub fn input_to_egui(
|
||||||
input_state.raw.mouse_pos = None;
|
input_state.raw.mouse_pos = None;
|
||||||
}
|
}
|
||||||
ReceivedCharacter(ch) => {
|
ReceivedCharacter(ch) => {
|
||||||
if !input_state.cmd && printable_char(ch) {
|
if printable_char(ch)
|
||||||
|
&& !input_state.raw.modifiers.ctrl
|
||||||
|
&& !input_state.raw.modifiers.mac_cmd
|
||||||
|
{
|
||||||
input_state.raw.events.push(Event::Text(ch.to_string()));
|
input_state.raw.events.push(Event::Text(ch.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyboardInput { input, .. } => {
|
KeyboardInput { input, .. } => {
|
||||||
if let Some(virtual_keycode) = input.virtual_keycode {
|
if let Some(keycode) = input.virtual_keycode {
|
||||||
let is_command_key = if cfg!(target_os = "macos") {
|
let pressed = input.state == glutin::event::ElementState::Pressed;
|
||||||
matches!(virtual_keycode, VirtualKeyCode::LWin | VirtualKeyCode::RWin)
|
|
||||||
} else {
|
|
||||||
matches!(
|
|
||||||
virtual_keycode,
|
|
||||||
VirtualKeyCode::LControl | VirtualKeyCode::RControl
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_command_key {
|
if matches!(keycode, VirtualKeyCode::LAlt | VirtualKeyCode::RAlt) {
|
||||||
input_state.cmd = input.state == glutin::event::ElementState::Pressed;
|
input_state.raw.modifiers.alt = pressed;
|
||||||
|
}
|
||||||
|
if matches!(keycode, VirtualKeyCode::LControl | VirtualKeyCode::RControl) {
|
||||||
|
input_state.raw.modifiers.ctrl = pressed;
|
||||||
|
if !cfg!(target_os = "macos") {
|
||||||
|
input_state.raw.modifiers.command = pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(keycode, VirtualKeyCode::LShift | VirtualKeyCode::RShift) {
|
||||||
|
input_state.raw.modifiers.shift = pressed;
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "macos")
|
||||||
|
&& matches!(keycode, VirtualKeyCode::LWin | VirtualKeyCode::RWin)
|
||||||
|
{
|
||||||
|
input_state.raw.modifiers.mac_cmd = pressed;
|
||||||
|
input_state.raw.modifiers.command = pressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.state == glutin::event::ElementState::Pressed {
|
if pressed {
|
||||||
if cfg!(target_os = "macos")
|
if cfg!(target_os = "macos")
|
||||||
&& input_state.cmd
|
&& input_state.raw.modifiers.mac_cmd
|
||||||
&& virtual_keycode == VirtualKeyCode::Q
|
&& keycode == VirtualKeyCode::Q
|
||||||
{
|
{
|
||||||
*control_flow = ControlFlow::Exit;
|
*control_flow = ControlFlow::Exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
|
// VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
|
||||||
// so we detect these things manually:
|
// so we detect these things manually:
|
||||||
if input_state.cmd && virtual_keycode == VirtualKeyCode::X {
|
if input_state.raw.modifiers.command && keycode == VirtualKeyCode::X {
|
||||||
input_state.raw.events.push(Event::Cut);
|
input_state.raw.events.push(Event::Cut);
|
||||||
} else if input_state.cmd && virtual_keycode == VirtualKeyCode::C {
|
} else if input_state.raw.modifiers.command && keycode == VirtualKeyCode::C {
|
||||||
input_state.raw.events.push(Event::Copy);
|
input_state.raw.events.push(Event::Copy);
|
||||||
} else if input_state.cmd && virtual_keycode == VirtualKeyCode::V {
|
} else if input_state.raw.modifiers.command && keycode == VirtualKeyCode::V {
|
||||||
if let Some(clipboard) = clipboard {
|
if let Some(clipboard) = clipboard {
|
||||||
match clipboard.get_contents() {
|
match clipboard.get_contents() {
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
|
@ -107,10 +113,11 @@ pub fn input_to_egui(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(key) = translate_virtual_key_code(virtual_keycode) {
|
} else if let Some(key) = translate_virtual_key_code(keycode) {
|
||||||
input_state.raw.events.push(Event::Key {
|
input_state.raw.events.push(Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: input.state == glutin::event::ElementState::Pressed,
|
pressed,
|
||||||
|
modifiers: input_state.raw.modifiers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,19 +164,19 @@ pub fn translate_virtual_key_code(key: VirtualKeyCode) -> Option<egui::Key> {
|
||||||
End => Key::End,
|
End => Key::End,
|
||||||
PageDown => Key::PageDown,
|
PageDown => Key::PageDown,
|
||||||
PageUp => Key::PageUp,
|
PageUp => Key::PageUp,
|
||||||
Left => Key::Left,
|
Left => Key::ArrowLeft,
|
||||||
Up => Key::Up,
|
Up => Key::ArrowUp,
|
||||||
Right => Key::Right,
|
Right => Key::ArrowRight,
|
||||||
Down => Key::Down,
|
Down => Key::ArrowDown,
|
||||||
Back => Key::Backspace,
|
Back => Key::Backspace,
|
||||||
Return => Key::Enter,
|
Return => Key::Enter,
|
||||||
// Space => Key::Space,
|
|
||||||
Tab => Key::Tab,
|
Tab => Key::Tab,
|
||||||
|
|
||||||
LAlt | RAlt => Key::Alt,
|
A => Key::A,
|
||||||
LShift | RShift => Key::Shift,
|
K => Key::K,
|
||||||
LControl | RControl => Key::Control,
|
U => Key::U,
|
||||||
LWin | RWin => Key::Logo,
|
W => Key::W,
|
||||||
|
Z => Key::Z,
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -92,16 +92,18 @@ impl egui::app::TextureAllocator for webgl::Painter {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
// TODO: Just use RawInput?
|
|
||||||
/// Data gathered between frames.
|
/// Data gathered between frames.
|
||||||
/// Is translated to `egui::RawInput` at the start of each frame.
|
/// Is translated to `egui::RawInput` at the start of each frame.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct WebInput {
|
pub struct WebInput {
|
||||||
|
/// In native points (not same as Egui points)
|
||||||
pub mouse_pos: Option<egui::Pos2>,
|
pub mouse_pos: Option<egui::Pos2>,
|
||||||
pub mouse_down: bool, // TODO: which button
|
/// Is this a touch screen? If so, we ignore mouse events.
|
||||||
pub is_touch: bool,
|
pub is_touch: bool,
|
||||||
|
/// In native points (not same as Egui points)
|
||||||
pub scroll_delta: egui::Vec2,
|
pub scroll_delta: egui::Vec2,
|
||||||
pub events: Vec<egui::Event>,
|
|
||||||
|
pub raw: egui::RawInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebInput {
|
impl WebInput {
|
||||||
|
@ -111,13 +113,12 @@ impl WebInput {
|
||||||
let scroll_delta = std::mem::take(&mut self.scroll_delta) * scale;
|
let scroll_delta = std::mem::take(&mut self.scroll_delta) * scale;
|
||||||
let mouse_pos = self.mouse_pos.map(|mp| pos2(mp.x * scale, mp.y * scale));
|
let mouse_pos = self.mouse_pos.map(|mp| pos2(mp.x * scale, mp.y * scale));
|
||||||
egui::RawInput {
|
egui::RawInput {
|
||||||
mouse_down: self.mouse_down,
|
|
||||||
mouse_pos,
|
mouse_pos,
|
||||||
scroll_delta,
|
scroll_delta,
|
||||||
screen_size: screen_size_in_native_points().unwrap() * scale,
|
screen_size: screen_size_in_native_points().unwrap() * scale,
|
||||||
pixels_per_point: Some(pixels_per_point),
|
pixels_per_point: Some(pixels_per_point),
|
||||||
time: now_sec(),
|
time: now_sec(),
|
||||||
events: std::mem::take(&mut self.events),
|
..self.raw.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +128,7 @@ impl WebInput {
|
||||||
pub struct AppRunner {
|
pub struct AppRunner {
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
pub web_backend: WebBackend,
|
pub web_backend: WebBackend,
|
||||||
pub web_input: WebInput,
|
pub input: WebInput,
|
||||||
pub app: Box<dyn App>,
|
pub app: Box<dyn App>,
|
||||||
pub needs_repaint: bool, // TODO: move
|
pub needs_repaint: bool, // TODO: move
|
||||||
}
|
}
|
||||||
|
@ -138,7 +139,7 @@ impl AppRunner {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
pixels_per_point: native_pixels_per_point(),
|
pixels_per_point: native_pixels_per_point(),
|
||||||
web_backend,
|
web_backend,
|
||||||
web_input: Default::default(),
|
input: Default::default(),
|
||||||
app,
|
app,
|
||||||
needs_repaint: true, // TODO: move
|
needs_repaint: true, // TODO: move
|
||||||
})
|
})
|
||||||
|
@ -151,7 +152,7 @@ impl AppRunner {
|
||||||
pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
|
pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
|
||||||
resize_canvas_to_screen_size(self.web_backend.canvas_id());
|
resize_canvas_to_screen_size(self.web_backend.canvas_id());
|
||||||
|
|
||||||
let raw_input = self.web_input.new_frame(self.pixels_per_point);
|
let raw_input = self.input.new_frame(self.pixels_per_point);
|
||||||
self.web_backend.begin_frame(raw_input);
|
self.web_backend.begin_frame(raw_input);
|
||||||
|
|
||||||
let mut integration_context = egui::app::IntegrationContext {
|
let mut integration_context = egui::app::IntegrationContext {
|
||||||
|
|
|
@ -187,14 +187,38 @@ pub fn location_hash() -> Option<String> {
|
||||||
web_sys::window()?.location().hash().ok()
|
web_sys::window()?.location().hash().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Web sends all all keys as strings, so it is up to us to figure out if it is
|
/// Web sends all keys as strings, so it is up to us to figure out if it is
|
||||||
/// a real text input or the name of a key.
|
/// a real text input or the name of a key.
|
||||||
fn should_ignore_key(key: &str) -> bool {
|
fn should_ignore_key(key: &str) -> bool {
|
||||||
let is_function_key = key.starts_with('F') && key.len() > 1;
|
let is_function_key = key.starts_with('F') && key.len() > 1;
|
||||||
is_function_key
|
is_function_key
|
||||||
|| matches!(
|
|| matches!(
|
||||||
key,
|
key,
|
||||||
"CapsLock" | "ContextMenu" | "NumLock" | "Pause" | "ScrollLock"
|
"Alt"
|
||||||
|
| "ArrowDown"
|
||||||
|
| "ArrowLeft"
|
||||||
|
| "ArrowRight"
|
||||||
|
| "ArrowUp"
|
||||||
|
| "Backspace"
|
||||||
|
| "CapsLock"
|
||||||
|
| "ContextMenu"
|
||||||
|
| "Control"
|
||||||
|
| "Delete"
|
||||||
|
| "End"
|
||||||
|
| "Enter"
|
||||||
|
| "Esc"
|
||||||
|
| "Escape"
|
||||||
|
| "Help"
|
||||||
|
| "Home"
|
||||||
|
| "Insert"
|
||||||
|
| "Meta"
|
||||||
|
| "NumLock"
|
||||||
|
| "PageDown"
|
||||||
|
| "PageUp"
|
||||||
|
| "Pause"
|
||||||
|
| "ScrollLock"
|
||||||
|
| "Shift"
|
||||||
|
| "Tab"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,24 +226,25 @@ fn should_ignore_key(key: &str) -> bool {
|
||||||
/// a real text input or the name of a key.
|
/// a real text input or the name of a key.
|
||||||
pub fn translate_key(key: &str) -> Option<egui::Key> {
|
pub fn translate_key(key: &str) -> Option<egui::Key> {
|
||||||
match key {
|
match key {
|
||||||
"Alt" => Some(egui::Key::Alt),
|
"ArrowDown" => Some(egui::Key::ArrowDown),
|
||||||
|
"ArrowLeft" => Some(egui::Key::ArrowLeft),
|
||||||
|
"ArrowRight" => Some(egui::Key::ArrowRight),
|
||||||
|
"ArrowUp" => Some(egui::Key::ArrowUp),
|
||||||
"Backspace" => Some(egui::Key::Backspace),
|
"Backspace" => Some(egui::Key::Backspace),
|
||||||
"Control" => Some(egui::Key::Control),
|
|
||||||
"Delete" => Some(egui::Key::Delete),
|
"Delete" => Some(egui::Key::Delete),
|
||||||
"ArrowDown" => Some(egui::Key::Down),
|
|
||||||
"End" => Some(egui::Key::End),
|
"End" => Some(egui::Key::End),
|
||||||
|
"Enter" => Some(egui::Key::Enter),
|
||||||
"Esc" | "Escape" => Some(egui::Key::Escape),
|
"Esc" | "Escape" => Some(egui::Key::Escape),
|
||||||
"Home" => Some(egui::Key::Home),
|
|
||||||
"Help" | "Insert" => Some(egui::Key::Insert),
|
"Help" | "Insert" => Some(egui::Key::Insert),
|
||||||
"ArrowLeft" => Some(egui::Key::Left),
|
"Home" => Some(egui::Key::Home),
|
||||||
"Meta" => Some(egui::Key::Logo),
|
|
||||||
"PageDown" => Some(egui::Key::PageDown),
|
"PageDown" => Some(egui::Key::PageDown),
|
||||||
"PageUp" => Some(egui::Key::PageUp),
|
"PageUp" => Some(egui::Key::PageUp),
|
||||||
"Enter" => Some(egui::Key::Enter),
|
|
||||||
"ArrowRight" => Some(egui::Key::Right),
|
|
||||||
"Shift" => Some(egui::Key::Shift),
|
|
||||||
"Tab" => Some(egui::Key::Tab),
|
"Tab" => Some(egui::Key::Tab),
|
||||||
"ArrowUp" => Some(egui::Key::Up),
|
"a" | "A" => Some(egui::Key::A),
|
||||||
|
"k" | "K" => Some(egui::Key::K),
|
||||||
|
"u" | "U" => Some(egui::Key::U),
|
||||||
|
"w" | "W" => Some(egui::Key::W),
|
||||||
|
"z" | "Z" => Some(egui::Key::Z),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,19 +292,24 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
// https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
// https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
|
let modifiers = modifiers_from_event(&event);
|
||||||
|
runner_lock.input.raw.modifiers = modifiers;
|
||||||
|
|
||||||
let key = event.key();
|
let key = event.key();
|
||||||
if !should_ignore_key(&key) {
|
|
||||||
if let Some(key) = translate_key(&key) {
|
if let Some(key) = translate_key(&key) {
|
||||||
runner_lock
|
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||||
.web_input
|
key,
|
||||||
.events
|
pressed: true,
|
||||||
.push(egui::Event::Key { key, pressed: true });
|
modifiers,
|
||||||
} else {
|
});
|
||||||
runner_lock.web_input.events.push(egui::Event::Text(key));
|
}
|
||||||
|
if !modifiers.ctrl && !modifiers.command && !should_ignore_key(&key) {
|
||||||
|
runner_lock.input.raw.events.push(egui::Event::Text(key));
|
||||||
}
|
}
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?;
|
document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?;
|
||||||
closure.forget();
|
closure.forget();
|
||||||
|
@ -290,14 +320,16 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
let key = event.key();
|
let modifiers = modifiers_from_event(&event);
|
||||||
if let Some(key) = translate_key(&key) {
|
runner_lock.input.raw.modifiers = modifiers;
|
||||||
runner_lock.web_input.events.push(egui::Event::Key {
|
if let Some(key) = translate_key(&event.key()) {
|
||||||
|
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
|
modifiers,
|
||||||
});
|
});
|
||||||
runner_lock.needs_repaint = true;
|
|
||||||
}
|
}
|
||||||
|
runner_lock.needs_repaint = true;
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?;
|
document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?;
|
||||||
closure.forget();
|
closure.forget();
|
||||||
|
@ -315,6 +347,22 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
|
||||||
|
egui::Modifiers {
|
||||||
|
alt: event.alt_key(),
|
||||||
|
ctrl: event.ctrl_key(),
|
||||||
|
shift: event.shift_key(),
|
||||||
|
|
||||||
|
// Ideally we should know if we are running or mac or not,
|
||||||
|
// but this works good enough for now.
|
||||||
|
mac_cmd: event.meta_key(),
|
||||||
|
|
||||||
|
// Ideally we should know if we are running or mac or not,
|
||||||
|
// but this works good enough for now.
|
||||||
|
command: event.ctrl_key() || event.meta_key(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap();
|
let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap();
|
||||||
|
@ -324,10 +372,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
if !runner_lock.web_input.is_touch {
|
if !runner_lock.input.is_touch {
|
||||||
runner_lock.web_input.mouse_pos =
|
runner_lock.input.mouse_pos =
|
||||||
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
||||||
runner_lock.web_input.mouse_down = true;
|
runner_lock.input.raw.mouse_down = true;
|
||||||
runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead
|
runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
|
@ -343,8 +391,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
if !runner_lock.web_input.is_touch {
|
if !runner_lock.input.is_touch {
|
||||||
runner_lock.web_input.mouse_pos =
|
runner_lock.input.mouse_pos =
|
||||||
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
|
@ -360,10 +408,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
if !runner_lock.web_input.is_touch {
|
if !runner_lock.input.is_touch {
|
||||||
runner_lock.web_input.mouse_pos =
|
runner_lock.input.mouse_pos =
|
||||||
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
|
||||||
runner_lock.web_input.mouse_down = false;
|
runner_lock.input.raw.mouse_down = false;
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
@ -378,8 +426,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
if !runner_lock.web_input.is_touch {
|
if !runner_lock.input.is_touch {
|
||||||
runner_lock.web_input.mouse_pos = None;
|
runner_lock.input.mouse_pos = None;
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
@ -394,9 +442,9 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
runner_lock.web_input.is_touch = true;
|
runner_lock.input.is_touch = true;
|
||||||
runner_lock.web_input.mouse_pos = Some(pos_from_touch_event(&event));
|
runner_lock.input.mouse_pos = Some(pos_from_touch_event(&event));
|
||||||
runner_lock.web_input.mouse_down = true;
|
runner_lock.input.raw.mouse_down = true;
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
@ -410,8 +458,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
runner_lock.web_input.is_touch = true;
|
runner_lock.input.is_touch = true;
|
||||||
runner_lock.web_input.mouse_pos = Some(pos_from_touch_event(&event));
|
runner_lock.input.mouse_pos = Some(pos_from_touch_event(&event));
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
@ -425,10 +473,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
runner_lock.web_input.is_touch = true;
|
runner_lock.input.is_touch = true;
|
||||||
runner_lock.web_input.mouse_down = false; // First release mouse to click...
|
runner_lock.input.raw.mouse_down = false; // First release mouse to click...
|
||||||
runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead)
|
runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead)
|
||||||
runner_lock.web_input.mouse_pos = None; // ...remove hover effect
|
runner_lock.input.mouse_pos = None; // ...remove hover effect
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
@ -442,8 +490,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
let runner_ref = runner_ref.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
let closure = Closure::wrap(Box::new(move |event: web_sys::WheelEvent| {
|
let closure = Closure::wrap(Box::new(move |event: web_sys::WheelEvent| {
|
||||||
let mut runner_lock = runner_ref.0.lock();
|
let mut runner_lock = runner_ref.0.lock();
|
||||||
runner_lock.web_input.scroll_delta.x -= event.delta_x() as f32;
|
runner_lock.input.scroll_delta.x -= event.delta_x() as f32;
|
||||||
runner_lock.web_input.scroll_delta.y -= event.delta_y() as f32;
|
runner_lock.input.scroll_delta.y -= event.delta_y() as f32;
|
||||||
runner_lock.needs_repaint = true;
|
runner_lock.needs_repaint = true;
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
|
Loading…
Reference in a new issue