mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
Docking: a native floating window publishes its IME descriptor after its editor or its last body
finish_ime after a body drawn before the Viewer cleared the preedit and made the Viewer's publication a new context, so a window with another group before the Viewer interrupted the composition every pass. Each native window now counts its bodies from the layout and publishes once its editor has recorded its geometry, or after its last body. A window that drew fewer bodies than counted publishes after every body in its next pass.
This commit is contained in:
+116
-10
@@ -7,7 +7,9 @@
|
||||
//! share one route.
|
||||
|
||||
use crate::state::UiState;
|
||||
use craft_ui::docking::{Action, Divider, DockArea, DockCommand, DockContent, DockError, DockEvent, DockStyle, FloatSpace, Hit, ResizeEdge, UiDock};
|
||||
use craft_ui::docking::{
|
||||
Action, Divider, DockArea, DockCommand, DockContent, DockError, DockEvent, DockStyle, FloatSpace, Floating, Hit, Node, ResizeEdge, UiDock,
|
||||
};
|
||||
use egui::{Rect, Sense, Stroke, StrokeKind, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -435,6 +437,7 @@ impl EffectcraftApp {
|
||||
child.set_clip_rect(body.intersect(ui.clip_rect()));
|
||||
let docs = self.tab_docs();
|
||||
let doc_width = dock::doc_tabs_width(ui, &docs);
|
||||
let ime = NativeIme::new(&layout.floating, &self.unpublished_viewports);
|
||||
let mut content = EffectContent {
|
||||
titles: self.tab_titles(),
|
||||
swatches: self.tab_swatches(),
|
||||
@@ -446,13 +449,14 @@ impl EffectcraftApp {
|
||||
locks: Vec::new(),
|
||||
menu: None,
|
||||
bodies: Vec::new(),
|
||||
viewports: Vec::new(),
|
||||
ime,
|
||||
};
|
||||
let mut output = area.show_content(&mut child, &layout, &style, &mut content);
|
||||
let EffectContent { doc_actions, locks, menu, bodies, viewports, .. } = content;
|
||||
let EffectContent { doc_actions, locks, menu, bodies, ime, .. } = content;
|
||||
self.dock_rects = bodies;
|
||||
self.unpublished_viewports = ime.unpublished();
|
||||
// A window that closed or docked takes its composition with it.
|
||||
let previous = std::mem::replace(&mut self.native_viewports, viewports);
|
||||
let previous = std::mem::replace(&mut self.native_viewports, ime.begun());
|
||||
let gone: Vec<_> = previous.into_iter().filter(|v| !self.native_viewports.contains(v)).collect();
|
||||
for viewport in gone {
|
||||
panels::viewer_text::end_viewport(self, &ctx, viewport);
|
||||
@@ -637,8 +641,110 @@ struct EffectContent<'a> {
|
||||
menu: Option<(PanelKind, egui::Pos2)>,
|
||||
/// Where each panel body was drawn, in drawing order (floating windows last).
|
||||
bodies: Vec<(PanelKind, Rect)>,
|
||||
/// The floating panels' OS windows whose pass has begun this frame.
|
||||
viewports: Vec<egui::ViewportId>,
|
||||
/// Where each native window's IME descriptor is set up and published.
|
||||
ime: NativeIme,
|
||||
}
|
||||
|
||||
/// Which body of a native floating window publishes its IME descriptor.
|
||||
///
|
||||
/// Each OS window is an egui pass of its own. `viewer_text::finish_ime` publishes what the pass's
|
||||
/// text editor recorded, and with nothing recorded it clears the composition and the preedit. A
|
||||
/// window with another group before the Viewer that published after every body would clear the
|
||||
/// preedit before the Viewer drew, and the Viewer's publication would then be a new context that
|
||||
/// interrupts the composition, every pass. So a window publishes after the body in which its
|
||||
/// editor has recorded its geometry and after its last body.
|
||||
pub(crate) struct NativeIme {
|
||||
/// The bodies each native window draws this pass, as its layout says.
|
||||
expected: Vec<(egui::ViewportId, usize)>,
|
||||
/// The bodies each native window has drawn so far, in the order the windows began.
|
||||
drawn: Vec<(egui::ViewportId, usize)>,
|
||||
/// Windows that published this pass.
|
||||
published: Vec<egui::ViewportId>,
|
||||
}
|
||||
|
||||
impl NativeIme {
|
||||
/// `unpublished` are the windows that drew last pass but never published: the layout's count
|
||||
/// was too high for them, so they publish after every body this pass.
|
||||
pub(crate) fn new(floating: &[Floating<PanelKind>], unpublished: &[egui::ViewportId]) -> Self {
|
||||
let expected = floating
|
||||
.iter()
|
||||
.filter_map(|window| {
|
||||
let viewport = window_viewport(window.id)?;
|
||||
let count = if unpublished.contains(&viewport) { 0 } else { window_bodies(window) };
|
||||
Some((viewport, count))
|
||||
})
|
||||
.collect();
|
||||
Self { expected, drawn: Vec::new(), published: Vec::new() }
|
||||
}
|
||||
|
||||
/// Count a body of `viewport` that starts drawing. True for the window's first body: its
|
||||
/// pass sets up the IME before it.
|
||||
pub(crate) fn begin_body(&mut self, viewport: egui::ViewportId) -> bool {
|
||||
match self.drawn.iter_mut().find(|(v, _)| *v == viewport) {
|
||||
Some((_, n)) => {
|
||||
*n = n.saturating_add(1);
|
||||
false
|
||||
}
|
||||
None => {
|
||||
self.drawn.push((viewport, 1));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the body of `viewport` that just drew publishes the IME descriptor: once the
|
||||
/// window's editor has `recorded` its geometry this pass, and after the window's last body.
|
||||
/// A window the layout does not name publishes after every body.
|
||||
pub(crate) fn publishes(&mut self, viewport: egui::ViewportId, recorded: bool) -> bool {
|
||||
let drawn = self.drawn.iter().find(|(v, _)| *v == viewport).map_or(0, |(_, n)| *n);
|
||||
let expected = self.expected.iter().find(|(v, _)| *v == viewport).map_or(0, |(_, n)| *n);
|
||||
let publishes = recorded || drawn >= expected;
|
||||
if publishes && !self.published.contains(&viewport) {
|
||||
self.published.push(viewport);
|
||||
}
|
||||
publishes
|
||||
}
|
||||
|
||||
/// The native windows whose pass began.
|
||||
pub(crate) fn begun(&self) -> Vec<egui::ViewportId> {
|
||||
self.drawn.iter().map(|(v, _)| *v).collect()
|
||||
}
|
||||
|
||||
/// The native windows that drew bodies but did not publish.
|
||||
pub(crate) fn unpublished(&self) -> Vec<egui::ViewportId> {
|
||||
self.drawn.iter().map(|(v, _)| *v).filter(|v| !self.published.contains(v)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The OS window craft-ui opens for the floating window `id`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn window_viewport(id: u64) -> Option<egui::ViewportId> {
|
||||
Some(DockArea::<PanelKind>::window_viewport(area_id(), id))
|
||||
}
|
||||
|
||||
/// The web keeps floating windows in the page.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn window_viewport(_id: u64) -> Option<egui::ViewportId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The panel bodies craft-ui draws in a floating window: the active panel of each group and each
|
||||
/// open stack entry. A minimized window shows its bar only.
|
||||
fn window_bodies(window: &Floating<PanelKind>) -> usize {
|
||||
if window.minimized {
|
||||
return 0;
|
||||
}
|
||||
let mut count = 0usize;
|
||||
let mut nodes = vec![&window.root];
|
||||
while let Some(node) = nodes.pop() {
|
||||
match node {
|
||||
Node::Split { first, second, .. } => nodes.extend([&**first, &**second]),
|
||||
Node::Tabs { panels, active } => count = count.saturating_add(usize::from(panels.get(*active).is_some())),
|
||||
Node::Stack { entries } => count = count.saturating_add(entries.iter().filter(|e| e.open).count()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
impl EffectContent<'_> {
|
||||
@@ -655,15 +761,15 @@ impl DockContent<PanelKind> for EffectContent<'_> {
|
||||
let rect = ui.max_rect();
|
||||
self.bodies.push((*panel, rect));
|
||||
// A floating panel's OS window is a pass of its own: its text IME descriptor is set up
|
||||
// before its body draws and published after (the main window does both in `draw`).
|
||||
// before its first body and published after its bodies (the main window does both in
|
||||
// `draw`).
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
let native = viewport != egui::ViewportId::ROOT && !ui.ctx().embed_viewports();
|
||||
if native && !self.viewports.contains(&viewport) {
|
||||
self.viewports.push(viewport);
|
||||
if native && self.ime.begin_body(viewport) {
|
||||
panels::viewer_text::begin_ui(ui.ctx());
|
||||
}
|
||||
self.app.panel_body(ui, *panel, rect);
|
||||
if native {
|
||||
if native && self.ime.publishes(viewport, panels::viewer_text::ime_recorded(ui.ctx())) {
|
||||
panels::viewer_text::finish_ime(self.app, ui);
|
||||
}
|
||||
// The body fills its frame; allocating it lets a floated panel keep its full size.
|
||||
|
||||
@@ -509,6 +509,8 @@ pub struct EffectcraftApp {
|
||||
/// The floating panels' OS windows that drew a panel last frame; one that is gone releases
|
||||
/// its IME state.
|
||||
pub(crate) native_viewports: Vec<egui::ViewportId>,
|
||||
/// The native windows that drew a panel last frame without publishing their IME descriptor.
|
||||
pub(crate) unpublished_viewports: Vec<egui::ViewportId>,
|
||||
/// A floating panel shown by a command: its window comes to the front next frame.
|
||||
pub(crate) pending_panel_raise: Option<PanelKind>,
|
||||
/// Home screen: recent-project thumbnail textures by path (None = no thumbnail).
|
||||
@@ -625,6 +627,7 @@ impl EffectcraftApp {
|
||||
recovery: None,
|
||||
dock_rects: vec![],
|
||||
native_viewports: vec![],
|
||||
unpublished_viewports: vec![],
|
||||
pending_panel_raise: None,
|
||||
home_thumbs: Default::default(),
|
||||
home_thumb_saved: None,
|
||||
|
||||
@@ -241,6 +241,13 @@ pub(crate) fn begin_ui(ctx: &egui::Context) {
|
||||
ctx.data_mut(|data| data.remove::<ImeGeometry>(id));
|
||||
}
|
||||
|
||||
/// Whether the text editor recorded its IME geometry in this pass of the current viewport.
|
||||
pub(crate) fn ime_recorded(ctx: &egui::Context) -> bool {
|
||||
let viewport = ctx.viewport_id();
|
||||
let pass = ctx.cumulative_pass_nr_for(viewport);
|
||||
ctx.data(|d| d.get_temp::<ImeGeometry>(ime_geometry_id(viewport))).is_some_and(|(p, _, _)| p == pass)
|
||||
}
|
||||
|
||||
pub(crate) fn finish_logic(app: &EffectcraftApp, ctx: &egui::Context) {
|
||||
let id = mode_id(ctx.viewport_id());
|
||||
if !ctx.data(|data| data.get_temp::<bool>(id).unwrap_or(false)) {
|
||||
@@ -1509,6 +1516,95 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// One native floating window holding the Timeline's group and then the Viewer's.
|
||||
fn two_group_window() -> craft_ui::docking::Floating<PanelKind> {
|
||||
let groups = vec![craft_ui::docking::Group::new(vec![PanelKind::Timeline], 0), craft_ui::docking::Group::new(vec![PanelKind::Viewer(1)], 0)];
|
||||
craft_ui::docking::Floating::stack(groups, [0.0, 0.0, 320.0, 360.0]).expect("two groups make a window")
|
||||
}
|
||||
|
||||
impl ImePass {
|
||||
/// One pass of a native window with a body that edits no text and the Viewer's editor,
|
||||
/// in the order given, publishing as `NativeIme` decides or, with `every`, after each
|
||||
/// body. Returns the descriptor and the preedit the pass ended with.
|
||||
fn window(
|
||||
&mut self,
|
||||
app: &mut EffectcraftApp,
|
||||
window: &craft_ui::docking::Floating<PanelKind>,
|
||||
events: Vec<egui::Event>,
|
||||
viewer_first: bool,
|
||||
every: bool,
|
||||
) -> (Option<egui::output::IMEOutput>, Option<String>) {
|
||||
let ctx = self.ctx.clone();
|
||||
let (rect, viewport) = (self.rect, self.viewport);
|
||||
let mut viewports = egui::ViewportIdMap::default();
|
||||
viewports.insert(viewport, egui::ViewportInfo::default());
|
||||
let raw = egui::RawInput { viewport_id: viewport, viewports, screen_rect: Some(rect), events, ..Default::default() };
|
||||
let mut preedit = None;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
let mut ime = crate::dock_ui::NativeIme::new(std::slice::from_ref(window), &[]);
|
||||
let order = if viewer_first { [true, false] } else { [false, true] };
|
||||
for viewer in order {
|
||||
if ime.begin_body(viewport) {
|
||||
begin_ui(ui.ctx());
|
||||
}
|
||||
if viewer {
|
||||
ui.interact(rect, focus_id(), egui::Sense::focusable_noninteractive());
|
||||
ui.memory_mut(|m| m.request_focus(focus_id()));
|
||||
sync_ime_target(ui, app);
|
||||
keyboard(app, ui, rect);
|
||||
let pass = ui.ctx().cumulative_pass_nr_for(viewport);
|
||||
ui.data_mut(|d| d.insert_temp(ime_geometry_id(viewport), (pass, rect, rect)));
|
||||
}
|
||||
if ime.publishes(viewport, ime_recorded(ui.ctx())) || every {
|
||||
finish_ime(app, ui);
|
||||
}
|
||||
}
|
||||
assert!(ime.unpublished().is_empty(), "the window published");
|
||||
preedit = read_preedit(ui).map(|p| p.text);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
(out.platform_output.ime, preedit)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_window_with_a_group_before_the_viewer_keeps_the_composition() {
|
||||
let window = two_group_window();
|
||||
let viewport = craft_ui::docking::DockArea::<PanelKind>::window_viewport(crate::dock_ui::area_id(), window.id);
|
||||
let preview = || egui::Event::Ime(egui::ImeEvent::Preedit { text: "é".into(), active_range_chars: None });
|
||||
for viewer_first in [false, true] {
|
||||
let (mut app, _) = editor("abcd", 4, 4);
|
||||
let mut pass = ImePass::new(viewport);
|
||||
let (first, preedit) = pass.window(&mut app, &window, vec![preview()], viewer_first, false);
|
||||
let first = first.expect("the editor publishes");
|
||||
assert_eq!(preedit.as_deref(), Some("é"));
|
||||
for _ in 0..2 {
|
||||
let (ime, preedit) = pass.window(&mut app, &window, vec![], viewer_first, false);
|
||||
let ime = ime.expect("the editor keeps publishing");
|
||||
assert!(!ime.should_interrupt_composition, "viewer first: {viewer_first}");
|
||||
assert_eq!((ime.owner, ime.context_generation), (first.owner, first.context_generation), "the same IME context");
|
||||
assert_eq!(preedit.as_deref(), Some("é"), "the preedit survives the other body");
|
||||
}
|
||||
}
|
||||
// Publishing after the body drawn before the Viewer clears the preedit and interrupts.
|
||||
let (mut app, _) = editor("abcd", 4, 4);
|
||||
let mut pass = ImePass::new(viewport);
|
||||
pass.window(&mut app, &window, vec![preview()], false, true);
|
||||
let (ime, preedit) = pass.window(&mut app, &window, vec![], false, true);
|
||||
assert!(ime.expect("the editor publishes").should_interrupt_composition);
|
||||
assert_eq!(preedit, None);
|
||||
// A window that drew fewer bodies than its layout counts publishes after every body in
|
||||
// its next pass.
|
||||
let mut ime = crate::dock_ui::NativeIme::new(std::slice::from_ref(&window), &[]);
|
||||
ime.begin_body(viewport);
|
||||
assert!(!ime.publishes(viewport, false));
|
||||
let unpublished = ime.unpublished();
|
||||
assert_eq!(unpublished, vec![viewport]);
|
||||
let mut ime = crate::dock_ui::NativeIme::new(std::slice::from_ref(&window), &unpublished);
|
||||
ime.begin_body(viewport);
|
||||
assert!(ime.publishes(viewport, false));
|
||||
}
|
||||
|
||||
fn ime_text(ime: &egui::output::IMEOutput) -> (&str, usize, usize) {
|
||||
let p = ime.surrounding_text.available().expect("committed surrounding text");
|
||||
(p.text.as_str(), p.cursor, p.anchor)
|
||||
|
||||
Reference in New Issue
Block a user