Skip to content

Commit 1fa72cc

Browse files
committed
improve cosmic-text integration, add full Ime support, improve text examples, fix most alignment issues, some performance fixes
- update cosmic-text to 0.18 - work around scrolling issues with cosmic text - fix textbox mouse dragging handling - fix textbox flickering - make RenderText calculate widest line of the entire text, and not just what's visible - slightly improve RenderText performance by reducing unnecessary work - cache widest line until text changed - cache size until text changed - don't prune layouting/shaping results until text changed - fix RTL text rendering - fix TextAtlas texture format with Color glyphs (Rgba8SrgbPremultiplied -> Rgba8Srgb) - fix TextAtlas erroneously and repeatedly re-rendering glyphs when Color glyphs are requested - cleanup text_renderer.rs a lot - add mechanism to clear TextAtlas state when fonts change - change default line height to 1.2 - mark blit as inline - add more checks for textbox's active state - add active field to TextBoxResponse - separate out logic for calculating the max line width into a function - add full Ime support with text_cursor in input_state and integrate it with winit and SDL - improve text examples, make them use system fonts, add ability to use system fonts - fix most alignment issues, add min_width and inline props to control alignment behavior - make yakui manage its own font selection instead of relying on cosmic-text's mechanisms also, a bunch of fixes with cursor and selection: - cursor renders correctly now (there are no longer cases where the cursor either disappears or renders on the wrong line due to cursor affinity) - ctrl+a works correctly now - tapping left after ctrl+a works correctly now (puts cursor to start of selection) - shifting while tapping up/down works correctly now - selecting whole word works correctly now - deselecting whole word works correctly now - deselecting characters works correctly now - selecting while cursor is inbetween a word works correctly now
1 parent 774ec67 commit 1fa72cc

File tree

32 files changed

+2270
-797
lines changed

32 files changed

+2270
-797
lines changed

crates/bootstrap/src/common.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
use core::cell::Cell;
2+
use std::sync::Arc;
3+
4+
use yakui::cosmic_text::fontdb;
5+
use yakui::font::Fonts;
16
use yakui::paint::{Texture, TextureFilter, TextureFormat};
2-
use yakui::{ManagedTextureId, TextureId, UVec2};
7+
use yakui::util::widget;
8+
use yakui::widget::Widget;
9+
use yakui::{ManagedTextureId, TextureId, UVec2, Vec2};
310

411
pub const OPENMOJI: &[u8] = include_bytes!("../assets/OpenMoji-color-glyf_colr_0.ttf");
512

@@ -112,3 +119,43 @@ pub fn get_backend() -> BootstrapBackend {
112119
})
113120
.unwrap_or(BootstrapBackend::Winit)
114121
}
122+
123+
#[derive(Debug)]
124+
struct LoadCommonFontsWidget {
125+
loaded: Cell<bool>,
126+
}
127+
128+
impl Widget for LoadCommonFontsWidget {
129+
type Props<'a> = ();
130+
131+
type Response = ();
132+
133+
fn new() -> Self {
134+
Self {
135+
loaded: Cell::default(),
136+
}
137+
}
138+
139+
fn update(&mut self, _props: Self::Props<'_>) -> Self::Response {}
140+
141+
fn layout(
142+
&self,
143+
ctx: yakui::widget::LayoutContext<'_>,
144+
_constraints: yakui::Constraints,
145+
) -> yakui::Vec2 {
146+
if !self.loaded.get() {
147+
let fonts = ctx.dom.get_global_or_init(Fonts::default);
148+
149+
fonts.load_system_fonts();
150+
fonts.load_font_source(fontdb::Source::Binary(Arc::from(&OPENMOJI)));
151+
152+
self.loaded.set(true);
153+
}
154+
155+
Vec2::ZERO
156+
}
157+
}
158+
159+
pub fn load_common_fonts() {
160+
widget::<LoadCommonFontsWidget>(());
161+
}

crates/bootstrap/src/with_winit.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,11 @@ impl<T: ExampleBody> ApplicationHandler for App<T> {
104104
_window_id: WindowId,
105105
event: WindowEvent,
106106
) {
107-
if self
108-
.yak_window
109-
.as_mut()
110-
.unwrap()
111-
.handle_window_event(&mut self.yak, &event)
112-
{
107+
if self.yak_window.as_mut().unwrap().handle_window_event(
108+
&mut self.yak,
109+
&event,
110+
self.window.as_ref().unwrap(),
111+
) {
113112
return;
114113
}
115114

crates/demo/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async fn run(event_loop: EventLoop<()>, window: Window) {
4747
}
4848

4949
Event::WindowEvent { event, .. } => {
50-
graphics.handle_window_event(&mut yak, &event, event_loop);
50+
graphics.handle_window_event(&mut yak, &event, event_loop, &window);
5151
}
5252
_ => (),
5353
})

crates/yakui-app/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,12 @@ impl Graphics {
184184
yak: &mut yakui::Yakui,
185185
event: &WindowEvent,
186186
event_loop: &ActiveEventLoop,
187+
window: &Window,
187188
) -> bool {
188189
// yakui_winit will return whether it handled an event. This means that
189190
// yakui believes it should handle that event exclusively, like if a
190191
// button in the UI was clicked.
191-
if self.window.handle_window_event(yak, event) {
192+
if self.window.handle_window_event(yak, event, window) {
192193
return true;
193194
}
194195

crates/yakui-core/src/event.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ pub enum Event {
4848
/// A Unicode codepoint was typed in the window.
4949
TextInput(char),
5050

51+
/// An input-method preedit event has been fired.
52+
///
53+
/// Follows winit's `Ime::Preedit`
54+
TextPreedit(String, Option<(usize, usize)>),
55+
5156
/// Request focus of a specific widget, or clear focus if `None`.
5257
RequestFocus(Option<WidgetId>),
5358
}
@@ -107,6 +112,9 @@ pub enum WidgetEvent {
107112
/// Text was sent to the widget.
108113
TextInput(char, Modifiers),
109114

115+
/// Preedit text was sent to the widget.
116+
TextPreedit(String, Option<(usize, usize)>),
117+
110118
/// The widget was focused or unfocused.
111119
FocusChanged(bool),
112120
}

crates/yakui-core/src/input/input_state.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use smallvec::SmallVec;
66

77
use crate::dom::{Dom, DomNode};
88
use crate::event::{Event, EventInterest, EventResponse, WidgetEvent};
9+
use crate::geometry::Rect;
910
use crate::id::WidgetId;
1011
use crate::layout::LayoutDom;
1112
use crate::navigation::{navigate, NavDirection};
@@ -38,6 +39,9 @@ pub struct InputState {
3839

3940
/// If set, text input should be active.
4041
text_input_enabled: Cell<bool>,
42+
43+
/// If there's a text input active with a cursor, this will be set and forwarded to the window.
44+
text_cursor: Cell<Option<Rect>>,
4145
}
4246

4347
#[derive(Debug)]
@@ -120,12 +124,14 @@ impl InputState {
120124
last_selection: Cell::new(None),
121125
pending_navigation: Cell::new(None),
122126
text_input_enabled: Cell::new(false),
127+
text_cursor: Cell::new(None),
123128
}
124129
}
125130

126131
/// Begin a new frame for input handling.
127132
pub fn start(&self, dom: &Dom, layout: &LayoutDom) {
128133
self.text_input_enabled.set(false);
134+
self.text_cursor.set(None);
129135
self.notify_selection(dom, layout);
130136
}
131137

@@ -155,6 +161,20 @@ impl InputState {
155161
self.text_input_enabled.get()
156162
}
157163

164+
/// Sets the text cursor. Should be called every update from an active text input.
165+
///
166+
/// Should be in physical pixels.
167+
pub fn set_text_cursor(&self, cursor: Rect) {
168+
self.text_cursor.set(Some(cursor));
169+
}
170+
171+
/// Gets the text cursor, if any.
172+
///
173+
/// Should be in physical pixels.
174+
pub fn get_text_cursor(&self) -> Option<Rect> {
175+
self.text_cursor.get()
176+
}
177+
158178
/// Returns the mouse position, or [`None`] if it's outside the window.
159179
pub fn mouse_pos(&self, layout: &LayoutDom) -> Option<Vec2> {
160180
self.mouse
@@ -182,33 +202,33 @@ impl InputState {
182202
&self,
183203
dom: &Dom,
184204
layout: &LayoutDom,
185-
event: &Event,
205+
event: Event,
186206
) -> EventResponse {
187207
let res = match event {
188208
Event::CursorMoved(pos) => {
189-
self.mouse_moved(dom, layout, *pos);
209+
self.mouse_moved(dom, layout, pos);
190210
EventResponse::Bubble
191211
}
192212
Event::MouseButtonChanged { button, down } => {
193213
// Left clicking clears selection, unless the widget handling the event sets the
194214
// same selection again
195-
if button == &MouseButton::One && *down {
215+
if button == MouseButton::One && down {
196216
self.selection.set(None);
197217
}
198-
let response = self.mouse_button_changed(dom, layout, *button, *down);
199218

200-
response
219+
self.mouse_button_changed(dom, layout, button, down)
201220
}
202-
Event::MouseScroll { delta } => self.send_mouse_scroll(dom, layout, *delta),
221+
Event::MouseScroll { delta } => self.send_mouse_scroll(dom, layout, delta),
203222
Event::KeyChanged {
204223
key,
205224
down,
206225
modifiers,
207-
} => self.keyboard_key_changed(dom, layout, *key, *down, *modifiers),
226+
} => self.keyboard_key_changed(dom, layout, key, down, modifiers),
208227
Event::ModifiersChanged(modifiers) => self.modifiers_changed(modifiers),
209-
Event::TextInput(c) => self.text_input(dom, layout, *c),
228+
Event::TextInput(c) => self.text_input(dom, layout, c),
229+
Event::TextPreedit(text, position) => self.text_preedit(dom, layout, text, position),
210230
Event::RequestFocus(id) => {
211-
self.set_selection(*id);
231+
self.set_selection(id);
212232
EventResponse::Bubble
213233
}
214234
_ => EventResponse::Bubble,
@@ -336,8 +356,36 @@ impl InputState {
336356
EventResponse::Bubble
337357
}
338358

339-
fn modifiers_changed(&self, modifiers: &Modifiers) -> EventResponse {
340-
self.modifiers.set(*modifiers);
359+
fn modifiers_changed(&self, modifiers: Modifiers) -> EventResponse {
360+
self.modifiers.set(modifiers);
361+
EventResponse::Bubble
362+
}
363+
364+
fn text_preedit(
365+
&self,
366+
dom: &Dom,
367+
layout: &LayoutDom,
368+
text: String,
369+
position: Option<(usize, usize)>,
370+
) -> EventResponse {
371+
let selected = self.selection.get();
372+
if let Some(id) = selected {
373+
let Some(layout_node) = layout.get(id) else {
374+
return EventResponse::Bubble;
375+
};
376+
377+
if layout_node
378+
.event_interest
379+
.contains(EventInterest::FOCUSED_KEYBOARD)
380+
{
381+
// Panic safety: if this node is in the layout DOM, it must be
382+
// in the DOM.
383+
let mut node = dom.get_mut(id).unwrap();
384+
let event = WidgetEvent::TextPreedit(text, position);
385+
return self.fire_event(dom, layout, id, &mut node, &event);
386+
}
387+
}
388+
341389
EventResponse::Bubble
342390
}
343391

crates/yakui-core/src/state.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ impl Yakui {
3535

3636
context::bind_dom(&self.dom);
3737

38-
let response = self.input.handle_event(&self.dom, &self.layout, &event);
39-
40-
if let Event::ViewportChanged(viewport) = event {
41-
self.layout.set_unscaled_viewport(viewport);
38+
if let Event::ViewportChanged(viewport) = &event {
39+
self.layout.set_unscaled_viewport(*viewport);
4240
}
4341

42+
let response = self.input.handle_event(&self.dom, &self.layout, event);
43+
4444
context::unbind_dom();
4545
response == EventResponse::Sink
4646
}
@@ -148,6 +148,11 @@ impl Yakui {
148148
self.input.text_input_enabled()
149149
}
150150

151+
/// Gets the text cursor, if any.
152+
pub fn get_text_cursor(&self) -> Option<Rect> {
153+
self.input.get_text_cursor()
154+
}
155+
151156
/// Requests focus of a specific widget, or clears focus if `None`.
152157
pub fn request_focus(&mut self, id: Option<WidgetId>) {
153158
self.handle_event(Event::RequestFocus(id));

crates/yakui-sdl3/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ impl YakuiSdl3 {
3838

3939
pub fn update(&mut self, window: &Window, state: &mut yakui_core::Yakui) {
4040
let new_value = state.text_input_enabled();
41-
4241
match (self.text_input_enabled, new_value) {
4342
(false, true) => unsafe {
4443
SDL_StartTextInput(window.raw());
@@ -48,6 +47,20 @@ impl YakuiSdl3 {
4847
},
4948
(true, true) | (false, false) => {}
5049
}
50+
self.text_input_enabled = new_value;
51+
52+
if self.text_input_enabled {
53+
if let Some(rect) = state.get_text_cursor() {
54+
let pos = rect.pos();
55+
let size = rect.size();
56+
57+
window.subsystem().text_input().set_rect(
58+
window,
59+
sdl3::rect::Rect::new(pos.x as i32, pos.y as i32, size.x as u32, size.y as u32),
60+
0,
61+
);
62+
}
63+
}
5164
}
5265

5366
pub fn handle_event(&mut self, state: &mut yakui_core::Yakui, event: &SdlEvent) -> bool {
@@ -122,6 +135,21 @@ impl YakuiSdl3 {
122135
false
123136
}
124137

138+
SdlEvent::TextEditing {
139+
text,
140+
start,
141+
length,
142+
..
143+
} => {
144+
let byte_lens = text.chars().map(|v| v.len_utf8()).collect::<Vec<_>>();
145+
let start = *start as usize;
146+
let end = start + *length as usize;
147+
let start = byte_lens[..start].iter().sum();
148+
let end = byte_lens[..end].iter().sum();
149+
150+
state.handle_event(Event::TextPreedit(text.clone(), Some((start, end))))
151+
}
152+
125153
SdlEvent::KeyDown {
126154
scancode, keymod, ..
127155
} => {

crates/yakui-widgets/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ default = ["default-fonts"]
1111

1212
# Include built-in fonts.
1313
default-fonts = []
14+
system-fonts = ["cosmic-text/fontconfig"]
1415

1516
[dependencies]
1617
yakui-core = { path = "../yakui-core", version = "0.3.0" }
1718

18-
cosmic-text = { version = "0.15.0", default-features = false, features = [
19+
cosmic-text = { version = "0.18.2", default-features = false, features = [
1920
"std",
2021
"swash",
2122
] }
23+
# see yakui_widgets::widgets::textbox::cosmic_text_util
24+
unicode-segmentation = { version = "1.10.1" }
25+
2226
sys-locale = "0.3.1"
2327

2428
log.workspace = true

0 commit comments

Comments
 (0)