mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
M14: distinguish qualification from publication (#4173)
This commit is contained in:
@@ -374,6 +374,8 @@ impl eframe::App for WebShell {
|
||||
app.finish_ime_logic(ctx);
|
||||
}
|
||||
|
||||
/// Forward host-gathered pointer events into this frame's raw input, then let the app apply its
|
||||
/// shared input hook before egui runs.
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
raw.events.extend(self.host.pointers.borrow_mut().take());
|
||||
if let Some(app) = &mut self.app {
|
||||
@@ -381,6 +383,8 @@ impl eframe::App for WebShell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read host paste and drop input once in the UI phase, hand files to the app's drop targets, run
|
||||
/// the app UI, then poll control replies.
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if let Some(app) = &mut self.app {
|
||||
|
||||
@@ -108,6 +108,8 @@ impl eframe::App for App {
|
||||
file_access::unconfined(|| self.logic_frame(ctx, frame));
|
||||
self.app.finish_ime_logic(ctx);
|
||||
}
|
||||
/// Restore the borderless caption, apply platform menu and pen input, and hand the frame to the
|
||||
/// app's raw-input hook while the keyboard user is unconfined.
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
// winit gives the window its caption back when it changes the window's flags (maximize,
|
||||
// restore, …): take it off again before the frame is laid out (#1758).
|
||||
|
||||
@@ -459,6 +459,7 @@ impl Session {
|
||||
self.with_tool_cx(view, |t, cx| t.ime_session_matches_document(cx))
|
||||
}
|
||||
|
||||
/// Whether the active tool currently owns keyboard text input, as the Type tool does while editing.
|
||||
pub fn tool_wants_text(&self) -> bool {
|
||||
self.tool.wants_text()
|
||||
}
|
||||
|
||||
@@ -798,6 +798,7 @@ pub enum ImeSurrounding {
|
||||
}
|
||||
|
||||
impl Default for ImeSurrounding {
|
||||
/// Report surrounding text as unavailable because no document is active.
|
||||
fn default() -> Self {
|
||||
Self::Unavailable("no document is active")
|
||||
}
|
||||
|
||||
@@ -86,6 +86,8 @@ type CachedLayout = ((u64, TextObject), Arc<TextLayout>);
|
||||
/// A mutex (not an `AtomicU64`) keeps this available on wasm targets without 64-bit atomics.
|
||||
static NEXT_EDIT_SESSION: Mutex<u64> = Mutex::new(0);
|
||||
|
||||
/// Return the next process-wide edit-session id. It saturates at `u64::MAX` instead of wrapping to
|
||||
/// zero; the final id can repeat only after 2^64 sessions.
|
||||
fn next_edit_session() -> u64 {
|
||||
let mut next = NEXT_EDIT_SESSION.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let session = next.checked_add(1).unwrap_or(u64::MAX);
|
||||
@@ -170,6 +172,8 @@ impl TypeTool {
|
||||
l
|
||||
}
|
||||
|
||||
/// The edited text used to find word and paragraph boundaries for selection: the typing session's
|
||||
/// runs, or the document's current text when there is no live session or it has gone stale.
|
||||
fn plain(&self, cx: &ToolContext) -> String {
|
||||
let document = self.editing.and_then(|id| Self::text(cx, id));
|
||||
match &self.typing {
|
||||
@@ -321,6 +325,8 @@ impl TypeTool {
|
||||
out
|
||||
}
|
||||
|
||||
/// Begin a new edit session for `id` at `caret`, assigning a fresh session id and clearing typing,
|
||||
/// pre-edit and goal state.
|
||||
fn start_editing(&mut self, id: NodeId, caret: usize) {
|
||||
self.edit_session = next_edit_session();
|
||||
self.editing = Some(id);
|
||||
@@ -1008,6 +1014,8 @@ impl Tool for TypeTool {
|
||||
let clip: String = self.clipboard.iter().map(|r| r.text.as_str()).collect();
|
||||
!(self.preedit.is_none() && normalized.len() > 1 && normalized == clip && !cx.paste_plain_text)
|
||||
}
|
||||
/// Whether the typing session still describes the edited document; a session ended by an outside
|
||||
/// command reports false until a replacement commit starts a new one.
|
||||
fn ime_session_matches_document(&self, cx: &ToolContext) -> bool {
|
||||
match (self.editing, &self.typing) {
|
||||
(Some(id), Some(ty)) => Self::text(cx, id).is_none_or(|t| ty.cur == t.runs),
|
||||
@@ -1017,6 +1025,8 @@ impl Tool for TypeTool {
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
/// Handle an editing key: ignore keys while an IME composes, remap arrows in vertical text, step
|
||||
/// type attributes with Alt, and otherwise edit, navigate or end the session.
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
// The IME owns the keys while it composes (the UI doesn't send them; this is a guard).
|
||||
if self.preedit.is_some() {
|
||||
@@ -1183,6 +1193,9 @@ impl Tool for TypeTool {
|
||||
self.preedit = Some(Preedit { range: start..end, active: active_chars.and_then(|r| char_range_to_bytes(&text, r)) });
|
||||
out
|
||||
}
|
||||
/// Delete the requested bytes or characters before and after the selection or marked text,
|
||||
/// preserving the selection direction and composition and refusing a stale session; byte counts
|
||||
/// round inward to UTF-8 boundaries.
|
||||
fn ime_delete_surrounding(&mut self, cx: &ToolContext, before: usize, after: usize, unit: SurroundingUnit) -> Vec<Action> {
|
||||
let Some(id) = self.editing else { return vec![] };
|
||||
// The document changed under the session (undo, a panel edit): the ranges below would index
|
||||
@@ -1235,11 +1248,16 @@ impl Tool for TypeTool {
|
||||
}
|
||||
self.delete_ranges(cx, &ranges)
|
||||
}
|
||||
/// Return the active Type tool's stable IME owner (document uid, target id, replacement epoch),
|
||||
/// or `None` when nothing is being edited.
|
||||
fn ime_target(&self, cx: &ToolContext) -> Option<(u64, u64, u64)> {
|
||||
let id = self.editing?;
|
||||
Self::text(cx, id)?;
|
||||
Some((cx.revision.0, id.0, self.edit_session))
|
||||
}
|
||||
/// Return a bounded logical text window around the caret, anchor and optional pre-edit; a stale
|
||||
/// typing session publishes the document's current text, and unusable or non-boundary offsets
|
||||
/// return `Unavailable` rather than stale data.
|
||||
fn ime_surrounding(&self, cx: &ToolContext, max_bytes: usize) -> ImeSurrounding {
|
||||
let Some(id) = self.editing else {
|
||||
return ImeSurrounding::Unavailable("no text object is being edited");
|
||||
@@ -1279,6 +1297,7 @@ impl Tool for TypeTool {
|
||||
};
|
||||
ime_surrounding_window(&segments, preedit, len, caret, anchor, max_bytes)
|
||||
}
|
||||
/// Whether an IME pre-edit is currently shown for this Type-tool session.
|
||||
fn composing(&self) -> bool {
|
||||
self.preedit.is_some()
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ pub(crate) fn end_document_drag(ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Temp-memory key for the active canvas drag state.
|
||||
fn drag_id() -> egui::Id {
|
||||
egui::Id::unique("canvas-drag")
|
||||
}
|
||||
@@ -3084,10 +3085,12 @@ fn ime_geometry(app: &mut VectorcraftApp, ctx: &egui::Context, xf: &Xf) {
|
||||
/// out rather than spilling over its neighbours.
|
||||
const MIN_LABELLED_ARTBOARD: f32 = 24.0;
|
||||
|
||||
/// Temp-memory key recording an artboard-name press before its edit field opens.
|
||||
fn name_press_id() -> egui::Id {
|
||||
egui::Id::unique("canvas-artboard-name-press")
|
||||
}
|
||||
|
||||
/// Temp-memory key for the artboard name field currently being edited.
|
||||
fn name_edit_id() -> egui::Id {
|
||||
egui::Id::unique("canvas-artboard-name-edit")
|
||||
}
|
||||
@@ -4689,6 +4692,8 @@ mod tests {
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no {text:?} text"))
|
||||
}
|
||||
/// Build raw input with a fixed 800 by 600 screen so headless frames keep a stable canvas texture
|
||||
/// size.
|
||||
fn raw() -> egui::RawInput {
|
||||
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() }
|
||||
}
|
||||
@@ -8755,6 +8760,8 @@ mod tests {
|
||||
assert_eq!(plain(&app, id), "雅がくらくか");
|
||||
}
|
||||
|
||||
/// Reset to a blank document, select the Type tool and click document point (100, 100), returning
|
||||
/// the screen position used by IME fixtures.
|
||||
fn type_at(app: &mut VectorcraftApp, ctx: &egui::Context) -> egui::Pos2 {
|
||||
app.session.prefs.placeholder_text = false;
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
|
||||
@@ -336,6 +336,8 @@ fn import(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draw the graph-data sheet: keyboard cell movement, block paste, import, transpose, switch-x/y
|
||||
/// and revert actions, decimals and column width, and only the cells in view.
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let entry_id = egui::Id::unique("graph-data-entry");
|
||||
if ui.memory(|m| m.has_focus(entry_id)) {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(crate) fn hint(d: &Dialog) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Temp-memory key for the revolve gizmo drag state.
|
||||
fn drag_id() -> Id {
|
||||
Id::unique("revolve-gizmo-drag")
|
||||
}
|
||||
|
||||
@@ -373,6 +373,7 @@ struct Cache {
|
||||
shots: Vec<(String, Result<Shot, String>)>,
|
||||
}
|
||||
|
||||
/// Temp-memory key for the Save for Web preview render, original image and optimised shot textures.
|
||||
fn cache_id() -> egui::Id {
|
||||
egui::Id::unique("save-for-web-previews")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::VectorcraftApp;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, VectorcraftApp>;
|
||||
|
||||
/// Build a 300 by 300 document for a real-dialog accessibility harness.
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
@@ -38,6 +39,7 @@ fn strict(app: VectorcraftApp) -> Harness {
|
||||
h
|
||||
}
|
||||
|
||||
/// Export for Screens names its three text fields and exposes no raw widget ids.
|
||||
#[test]
|
||||
fn export_for_screens_names_the_form_text_fields() {
|
||||
let mut app = app();
|
||||
@@ -50,6 +52,8 @@ fn export_for_screens_names_the_form_text_fields() {
|
||||
assert!(h.query_by_role_and_label(Role::TextInput, "prefix").is_none(), "no internal widget id");
|
||||
}
|
||||
|
||||
/// Flatten Transparency names its preset name and three resolution or balance fields and roles the
|
||||
/// balance control as a slider.
|
||||
#[test]
|
||||
fn flatten_transparency_names_its_preset_and_option_fields() {
|
||||
let mut app = app();
|
||||
@@ -66,6 +70,7 @@ fn flatten_transparency_names_its_preset_and_option_fields() {
|
||||
let _ = h.get_by_role_and_label(Role::Slider, "Raster/Vector Balance:");
|
||||
}
|
||||
|
||||
/// Document Setup names the top, bottom, left and right bleed inputs.
|
||||
#[test]
|
||||
fn document_setup_names_each_bleed_side() {
|
||||
let mut app = app();
|
||||
@@ -76,6 +81,8 @@ fn document_setup_names_each_bleed_side() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Split Into Grid keeps translated visible labels while naming row and column gutter and total
|
||||
/// inputs by axis and context.
|
||||
#[test]
|
||||
fn split_into_grid_names_row_and_column_fields_with_context() {
|
||||
let mut app = app();
|
||||
@@ -93,6 +100,7 @@ fn split_into_grid_names_row_and_column_fields_with_context() {
|
||||
assert_eq!(h.get_all_by_label("Total:").count(), 2, "the visible total labels are unchanged");
|
||||
}
|
||||
|
||||
/// Artboard Options names the video ruler pixel aspect ratio input.
|
||||
#[test]
|
||||
fn artboard_options_names_the_video_pixel_aspect_ratio_field() {
|
||||
let mut app = app();
|
||||
@@ -103,6 +111,7 @@ fn artboard_options_names_the_video_pixel_aspect_ratio_field() {
|
||||
let _ = h.get_by_role_and_label(Role::TextInput, "Video Ruler Pixel Aspect Ratio:");
|
||||
}
|
||||
|
||||
/// A generic parameter dialog names its number and text inputs, combo boxes and checkbox.
|
||||
#[test]
|
||||
fn generic_parameter_dialog_names_numbers_text_and_choices() {
|
||||
let mut app = app();
|
||||
@@ -130,6 +139,7 @@ fn generic_parameter_dialog_names_numbers_text_and_choices() {
|
||||
let _ = h.get_by_role_and_label(Role::CheckBox, "Flip:");
|
||||
}
|
||||
|
||||
/// Slice Options names its multiline text-display area.
|
||||
#[test]
|
||||
fn slice_options_names_its_text_area() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -56,6 +56,7 @@ impl Harness {
|
||||
self.texts.iter().find(|(s, _)| s == text).map(|(_, r)| *r).unwrap_or_else(|| panic!("missing text {text:?}"))
|
||||
}
|
||||
|
||||
/// Return the rectangle of the widget drawn under `Id::unique(id)` in the revolve test harness.
|
||||
fn response(&self, id: impl std::hash::Hash + std::fmt::Debug) -> Rect {
|
||||
self.ctx.read_response(Id::unique(id)).unwrap().rect
|
||||
}
|
||||
|
||||
@@ -333,6 +333,7 @@ pub(crate) fn button_id() -> egui::Id {
|
||||
egui::Id::unique("edit-toolbar-button")
|
||||
}
|
||||
|
||||
/// Stable id of the floating edit toolbar.
|
||||
pub(crate) fn toolbar_id() -> egui::Id {
|
||||
egui::Id::unique("edit-toolbar-toolbar")
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ struct OutputContext {
|
||||
}
|
||||
|
||||
impl From<&IMEOutput> for OutputContext {
|
||||
/// Copy the output's owner, context generation and purpose into the comparable publication
|
||||
/// identity.
|
||||
fn from(output: &IMEOutput) -> Self {
|
||||
Self { owner: output.owner, generation: output.context_generation, purpose: output.purpose }
|
||||
}
|
||||
@@ -64,9 +66,12 @@ pub(crate) struct ResetGuard {
|
||||
pending: bool,
|
||||
}
|
||||
|
||||
/// Temp-memory key marking whether this viewport is currently inside a UI pass.
|
||||
fn mode_id(viewport: egui::ViewportId) -> Id {
|
||||
Id::unique(("vectorcraft.ime.in-ui-pass", viewport))
|
||||
}
|
||||
/// Temp-memory key for the pass number and descriptor identity observed during this viewport's UI
|
||||
/// pass.
|
||||
fn output_id(viewport: egui::ViewportId) -> Id {
|
||||
Id::unique(("vectorcraft.ime.pass-output", viewport))
|
||||
}
|
||||
@@ -133,6 +138,9 @@ fn route_live(app: &mut VectorcraftApp, ctx: &Context) -> bool {
|
||||
target(&mut app.session, view).is_some() && app.session.tool_ime_caret(view).is_some()
|
||||
}
|
||||
|
||||
/// Allocate a reset request on an eligible custom-editor route and replace the guard with the
|
||||
/// confirmed destination, commit stamp and prior output identity; suspend instead when the route
|
||||
/// cannot publish.
|
||||
fn request_guard(app: &mut VectorcraftApp, ctx: &Context, deferred: bool) -> bool {
|
||||
if !route_live(app, ctx) {
|
||||
suspend(app, ctx);
|
||||
@@ -163,11 +171,14 @@ fn request_guard(app: &mut VectorcraftApp, ctx: &Context, deferred: bool) -> boo
|
||||
true
|
||||
}
|
||||
|
||||
/// Temp-memory key for the test-only count of IME reset requests since the last take.
|
||||
#[cfg(test)]
|
||||
fn request_count_id() -> Id {
|
||||
Id::unique("vectorcraft.ime.test-request-count")
|
||||
}
|
||||
|
||||
/// Increment the test-only request counter for every reset allocation, including intermediates
|
||||
/// superseded before publication.
|
||||
#[cfg(test)]
|
||||
fn note_test_request(ctx: &Context) {
|
||||
ctx.data_mut(|data| {
|
||||
@@ -202,10 +213,12 @@ pub(crate) fn suspend_if_route_lost(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this viewport has a guard, including a suspended guard that currently owns no request.
|
||||
pub(crate) fn blocked(app: &VectorcraftApp, ctx: &Context) -> bool {
|
||||
app.ime_resets.contains_key(&ctx.viewport_id())
|
||||
}
|
||||
|
||||
/// Return the reset request awaiting native completion for this viewport; test helper.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pending_request(app: &VectorcraftApp, ctx: &Context) -> Option<egui::ImeResetId> {
|
||||
app.ime_resets.get(&ctx.viewport_id()).and_then(|guard| guard.request)
|
||||
@@ -306,6 +319,8 @@ fn mark_pending(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
app.ime_compositions.insert(viewport, IncomingComposition::Stale);
|
||||
}
|
||||
|
||||
/// Retire the guard's request and clear its deferred and pending flags while keeping the guard
|
||||
/// blocked until an eligible route allocates a replacement.
|
||||
fn suspend(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
if let Some(guard) = app.ime_resets.get_mut(&ctx.viewport_id()) {
|
||||
guard.request = None;
|
||||
@@ -378,6 +393,8 @@ pub(crate) fn controls_without_editor(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the previous pass's caret rectangle and published descriptor identity before this
|
||||
/// viewport's UI runs.
|
||||
pub(crate) fn begin_ui(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
app.ime_caret_rect = None;
|
||||
let id = output_id(ctx.viewport_id());
|
||||
@@ -418,15 +435,21 @@ pub(crate) fn finish_ui(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this pass can publish a custom editor and the descriptor recorded during the pass is the
|
||||
/// one actually published.
|
||||
fn eligible(app: &VectorcraftApp, ctx: &Context, custom: Option<OutputContext>) -> bool {
|
||||
let actual = ctx.output(|output| output.ime.as_ref().map(OutputContext::from));
|
||||
eligible_with(app, ctx, custom, actual)
|
||||
}
|
||||
|
||||
/// Require a custom descriptor, a matching actual output and an eligible route before an IME guard
|
||||
/// may settle or re-arm.
|
||||
fn eligible_with(app: &VectorcraftApp, ctx: &Context, custom: Option<OutputContext>, actual: Option<OutputContext>) -> bool {
|
||||
custom.is_some() && actual == custom && pass_eligible(app, ctx)
|
||||
}
|
||||
|
||||
/// Whether a guard needs a new fence because it was suspended, deferred or pending, its destination
|
||||
/// changed, or its published descriptor identity changed.
|
||||
fn needs_replacement(guard: &ResetGuard, destination: Option<IncomingSelection>, custom: OutputContext) -> bool {
|
||||
guard.suspended || guard.deferred || guard.pending || guard.destination != destination || guard.output.is_some_and(|previous| previous != custom)
|
||||
}
|
||||
@@ -443,6 +466,8 @@ pub(crate) fn finish_logic(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the current Type-tool target and its anchor and caret offsets, or `None` when no text edit
|
||||
/// target or selection options are available.
|
||||
fn incoming_selection(app: &mut VectorcraftApp) -> Option<IncomingSelection> {
|
||||
let view = app.view_info();
|
||||
let target = target(&mut app.session, view)?;
|
||||
@@ -548,6 +573,9 @@ fn sync_incoming_unbound(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
sync_incoming_impl(app, ctx, Fence::Unbound);
|
||||
}
|
||||
|
||||
/// Settle an incoming composition whose selection, composing state, session or context no longer
|
||||
/// matches the recorded expectation, applying the fence policy; with no recorded composition, also
|
||||
/// retire or replace a guard whose destination, commit stamp, context or pending state changed.
|
||||
fn sync_incoming_impl(app: &mut VectorcraftApp, ctx: &Context, fence: Fence) {
|
||||
let previous = app.ime_compositions.get(&ctx.viewport_id()).copied();
|
||||
let expected = match previous {
|
||||
@@ -774,6 +802,8 @@ pub(crate) fn write_output(ctx: &Context, update: Option<ImeContextUpdate>, rect
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A changed context generation or IME purpose for the same owner requires a replacement reset,
|
||||
/// while an unchanged descriptor keeps the existing request.
|
||||
#[test]
|
||||
fn same_owner_new_generation_or_purpose_requires_a_replacement_request() {
|
||||
let owner = Id::unique("vectorcraft-ime-output-context-test");
|
||||
|
||||
@@ -1961,6 +1961,8 @@ impl VectorcraftApp {
|
||||
|| self.pending_screenshots.iter().any(|p| p.slot.is_none())
|
||||
}
|
||||
|
||||
/// Capture the viewport identity and geometry (content and inner rectangles, pixels per point and
|
||||
/// zoom) whose change advances the screenshot epoch.
|
||||
fn geometry_stamp(ctx: &egui::Context) -> GeometryStamp {
|
||||
GeometryStamp {
|
||||
viewport: ctx.viewport_id(),
|
||||
@@ -2065,6 +2067,8 @@ impl VectorcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Identify the document, modal dialog and focused widget that the current accepted input batch
|
||||
/// belongs to.
|
||||
fn input_generation(&self) -> InputGeneration {
|
||||
InputGeneration {
|
||||
document: self.session.active().map_or(0, |document| document.uid),
|
||||
|
||||
@@ -1537,6 +1537,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Both stroke rows expose named Weight inputs, and the drop-shadow effect names its Blur input and
|
||||
/// Color radio button.
|
||||
#[test]
|
||||
fn inline_stroke_weight_and_effect_fields_are_named() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
||||
@@ -1211,6 +1211,7 @@ mod tests {
|
||||
assert_eq!((app.session.tool_id(), app.session.tool_options()), ("eyedropper", json!({"stop": "gradient", "brushKeyColor": false})));
|
||||
}
|
||||
|
||||
/// The gradient panel names its Angle and Aspect Ratio inputs.
|
||||
#[test]
|
||||
fn angle_and_aspect_fields_are_named() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
||||
@@ -126,10 +126,12 @@ struct ColumnDrag {
|
||||
ids: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Stable id for the layers panel's per-name UI state.
|
||||
fn key(name: &str) -> egui::Id {
|
||||
egui::Id::unique(("layers-panel", name))
|
||||
}
|
||||
|
||||
/// Temp-memory key for the layer rename editor.
|
||||
fn rename_id() -> egui::Id {
|
||||
egui::Id::unique("layers-rename")
|
||||
}
|
||||
|
||||
@@ -464,6 +464,7 @@ pub(crate) fn live_run(app: &mut VectorcraftApp, label: &str, cmd: &str, params:
|
||||
pub(crate) fn pstate<T: Clone + Default + Send + Sync + 'static>(ctx: &egui::Context, key: &str) -> T {
|
||||
ctx.data(|d| d.get_temp::<T>(egui::Id::unique(("panel-state", key)))).unwrap_or_default()
|
||||
}
|
||||
/// Store per-panel UI state in egui temp memory under the `panel-state` namespace.
|
||||
pub(crate) fn set_pstate<T: Clone + Send + Sync + 'static>(ctx: &egui::Context, key: &str, v: T) {
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique(("panel-state", key)), v));
|
||||
}
|
||||
|
||||
@@ -971,6 +971,7 @@ mod tests {
|
||||
assert_eq!(constrained(0.0, 50.0, Some(10.0), None), (10.0, 50.0));
|
||||
}
|
||||
|
||||
/// The Transform panel names its Rotate and Shear inputs.
|
||||
#[test]
|
||||
fn rotate_and_shear_fields_are_named() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
||||
@@ -30,6 +30,7 @@ fn label_id() -> Id {
|
||||
Id::unique("numeric-field-label")
|
||||
}
|
||||
|
||||
/// Temp-memory key for the numeric-field scrub preference.
|
||||
fn enabled_id() -> Id {
|
||||
Id::unique("numeric-field-scrub-enabled")
|
||||
}
|
||||
|
||||
@@ -514,6 +514,8 @@ fn apply_type_event(
|
||||
}
|
||||
}
|
||||
|
||||
/// Process one frame's events against the keymap at most once per context, viewport and frame, so
|
||||
/// reruns cannot fire a shortcut or tool key twice.
|
||||
pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
// One UI pass takes a frame's events. A discarded rerun and a test helper that also calls
|
||||
// `logic` see the same events again; they must not fire a shortcut or tool key twice. The
|
||||
|
||||
@@ -46,6 +46,7 @@ pub(crate) struct Publishing {
|
||||
}
|
||||
|
||||
impl Publishing {
|
||||
/// Whether the latest Copy or Cut still has formats waiting to be written to the system clipboard.
|
||||
pub(crate) fn is_pending(&self) -> bool {
|
||||
self.pending
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use vectorcraft_engine::Session;
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// Build a new 400 by 300 document with one selected rectangle for lifecycle fixtures.
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
@@ -16,10 +17,12 @@ fn app() -> VectorcraftApp {
|
||||
app
|
||||
}
|
||||
|
||||
/// Count child objects across every layer of the active document.
|
||||
fn objects(app: &VectorcraftApp) -> usize {
|
||||
app.session.active().map_or(0, |st| st.doc.layers.iter().map(|l| l.children().map_or(0, |c| c.len())).sum())
|
||||
}
|
||||
|
||||
/// Return the number of undo entries in the active document's history.
|
||||
fn undo(app: &VectorcraftApp) -> usize {
|
||||
app.session.active().map_or(0, |st| st.history.undo.len())
|
||||
}
|
||||
@@ -34,6 +37,8 @@ fn screen() -> egui::Rect {
|
||||
/// (which only pastes when the app copied text itself).
|
||||
const PASTE_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10"/></svg>"#;
|
||||
|
||||
/// A discarded rerun must execute a duplicate paste exactly once while the host iteration still
|
||||
/// ticks once.
|
||||
#[test]
|
||||
fn a_discarded_rerun_ticks_once_and_runs_a_duplicate_once() {
|
||||
let mut app = app();
|
||||
@@ -75,6 +80,8 @@ fn a_discarded_rerun_ticks_once_and_runs_a_duplicate_once() {
|
||||
assert_eq!(undo(&app), before_undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
/// Logic-only iterations advance the host clock but never read the stale raw events a hidden window
|
||||
/// replays.
|
||||
#[test]
|
||||
fn logic_only_iterations_tick_without_replaying_input() {
|
||||
let mut app = app();
|
||||
@@ -103,6 +110,8 @@ fn logic_only_iterations_tick_without_replaying_input() {
|
||||
assert_eq!(undo(&app), before_undo, "no command ran hidden");
|
||||
}
|
||||
|
||||
/// A visible frame consumes a paste once; later logic-only iterations with the same raw input must
|
||||
/// not replay it.
|
||||
#[test]
|
||||
fn a_consumed_event_is_not_replayed_by_a_later_logic_iteration() {
|
||||
let mut app = app();
|
||||
@@ -157,6 +166,8 @@ fn ime_frame(
|
||||
out.platform_output.ime
|
||||
}
|
||||
|
||||
/// Create a blank 400 by 300 document with one text object and return the app and node id;
|
||||
/// placeholder text is disabled so the object is the only text.
|
||||
fn app_with_text(text: &str) -> (VectorcraftApp, u64) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.prefs.placeholder_text = false;
|
||||
@@ -165,6 +176,7 @@ fn app_with_text(text: &str) -> (VectorcraftApp, u64) {
|
||||
(app, id)
|
||||
}
|
||||
|
||||
/// Read the plain text of the given text node in these lifecycle fixtures.
|
||||
fn plain(app: &VectorcraftApp, id: u64) -> String {
|
||||
match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
|
||||
vectorcraft_doc::NodeKind::Text(t) => t.plain_text(),
|
||||
@@ -172,6 +184,8 @@ fn plain(app: &VectorcraftApp, id: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a pointer move and a primary press/release pair through the production tick then UI
|
||||
/// lifecycle.
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, at: egui::Pos2) {
|
||||
let button =
|
||||
|pos: egui::Pos2, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
@@ -191,6 +205,7 @@ fn begin_typing(app: &mut VectorcraftApp, ctx: &egui::Context, id: u64) -> egui:
|
||||
at
|
||||
}
|
||||
|
||||
/// Build an IME pre-edit event whose active clause is an empty character range at `chars`.
|
||||
fn preedit(t: &str, chars: usize) -> Event {
|
||||
Event::Ime(egui::ImeEvent::Preedit { text: t.into(), active_range_chars: Some(chars..chars) })
|
||||
}
|
||||
@@ -210,6 +225,7 @@ fn interruption_editor() -> (VectorcraftApp, egui::Context, u64, egui::Pos2) {
|
||||
(app, ctx, id, point)
|
||||
}
|
||||
|
||||
/// Build a one-character or one-byte IME delete-surrounding event according to `bytes`.
|
||||
fn surrounding_delete(bytes: bool) -> Event {
|
||||
Event::Ime(if bytes {
|
||||
egui::ImeEvent::DeleteSurroundingBytes { before_bytes: 1, after_bytes: 0 }
|
||||
@@ -218,10 +234,13 @@ fn surrounding_delete(bytes: bool) -> Event {
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the reset-completed event for the app's current pending IME request.
|
||||
fn reset_marker(app: &VectorcraftApp, ctx: &egui::Context) -> Event {
|
||||
Event::ImeResetCompleted { request_id: crate::ime::pending_request(app, ctx).expect("a real interruption requested a fence") }
|
||||
}
|
||||
|
||||
/// Click the given point, assert the resulting custom descriptor publishes the real reset request,
|
||||
/// and return that request.
|
||||
fn pointer_reset(app: &mut VectorcraftApp, ctx: &egui::Context, point: egui::Pos2) -> egui::ImeResetId {
|
||||
let press = Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() };
|
||||
let output = ime_frame(app, ctx, vec![Event::PointerMoved(point), press], |_| {}).expect("the destination still edits text");
|
||||
@@ -232,10 +251,13 @@ fn pointer_reset(app: &mut VectorcraftApp, ctx: &egui::Context, point: egui::Pos
|
||||
request
|
||||
}
|
||||
|
||||
/// Build a pressed, non-repeat key event with the given key and modifiers.
|
||||
fn pressed(key: egui::Key, modifiers: egui::Modifiers) -> Event {
|
||||
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }
|
||||
}
|
||||
|
||||
/// A replacement fence admits an immediate fresh suffix without an old terminal, for empty and non-
|
||||
/// empty pre-edits and for same- and next-batch ordering.
|
||||
#[test]
|
||||
fn reset_fence_without_an_old_terminal_admits_the_immediate_fresh_suffix() {
|
||||
for terminal_pending in [false, true] {
|
||||
@@ -261,6 +283,8 @@ fn reset_fence_without_an_old_terminal_admits_the_immediate_fresh_suffix() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Only the newest reset request for this viewport, context and destination completes; old, foreign
|
||||
/// and duplicate markers cannot retire it or admit input.
|
||||
#[test]
|
||||
fn newest_full_reset_id_rejects_old_foreign_and_duplicate_controls() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -308,6 +332,8 @@ fn newest_full_reset_id_rejects_old_foreign_and_duplicate_controls() {
|
||||
assert_eq!(plain(&app, id), "abQRcdé", "a duplicate marker cannot retire the new live preview");
|
||||
}
|
||||
|
||||
/// A pressed navigation key retires the old fence, moves the caret once, and the final destination
|
||||
/// publishes a replacement fence before its suffix is accepted.
|
||||
#[test]
|
||||
fn pressed_navigation_retires_the_old_fence_until_the_real_destination_is_known() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -334,6 +360,8 @@ fn pressed_navigation_retires_the_old_fence_until_the_real_destination_is_known(
|
||||
assert_eq!(plain(&app, id), "abがcdé");
|
||||
}
|
||||
|
||||
/// A real permitted command key still runs while composing or after retirement; the cached
|
||||
/// composing flag must not swallow Cmd+R.
|
||||
#[test]
|
||||
fn a_pressed_command_runs_after_composition_retirement_and_after_its_fence() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -353,6 +381,8 @@ fn a_pressed_command_runs_after_composition_retirement_and_after_its_fence() {
|
||||
assert_eq!(app.ui.view.rulers, !rulers);
|
||||
}
|
||||
|
||||
/// A request allocated in a discarded pass is retired; an unavailable final pass leaves no token,
|
||||
/// and the same caret re-arms once when visible again.
|
||||
#[test]
|
||||
fn discarded_request_then_hidden_output_rearms_the_same_caret_once() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -397,6 +427,8 @@ fn discarded_request_then_hidden_output_rearms_the_same_caret_once() {
|
||||
assert_eq!(plain(&app, id), "abがcdé");
|
||||
}
|
||||
|
||||
/// Stock text remains editable while custom fences retire; returning from stock focus and losing
|
||||
/// window focus each allocate a fresh fence.
|
||||
#[test]
|
||||
fn stock_field_and_focus_loss_retire_custom_fences_without_consuming_stock_text() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -444,6 +476,8 @@ fn stock_field_and_focus_loss_retire_custom_fences_without_consuming_stock_text(
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// Text, paste and cut events before a deferred pointer's real handler are dropped; the pointer
|
||||
/// still moves the caret and publishes a replacement fence.
|
||||
#[test]
|
||||
fn a_deferred_pointer_drops_text_and_clipboard_until_its_real_handler_and_fence() {
|
||||
for paste in [false, true] {
|
||||
@@ -476,6 +510,8 @@ fn a_deferred_pointer_drops_text_and_clipboard_until_its_real_handler_and_fence(
|
||||
}
|
||||
}
|
||||
|
||||
/// A hidden route allocates no reset; an old marker and pointer suffix are inert and cannot lend a
|
||||
/// token to a later stock field.
|
||||
#[test]
|
||||
fn hidden_pointer_and_marker_leave_no_token_for_a_later_stock_field() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -516,6 +552,8 @@ fn hidden_pointer_and_marker_leave_no_token_for_a_later_stock_field() {
|
||||
assert!(crate::ime::pending_request(&app, &ctx).is_none());
|
||||
}
|
||||
|
||||
/// A hidden logic pass settles a changed composition without allocating, leaving no token for a
|
||||
/// later stock field.
|
||||
#[test]
|
||||
fn hidden_logic_with_a_changed_composition_leaves_no_token_for_a_later_stock_field() {
|
||||
let (mut app, ctx, id, _) = interruption_editor();
|
||||
@@ -542,6 +580,8 @@ fn hidden_logic_with_a_changed_composition_leaves_no_token_for_a_later_stock_fie
|
||||
assert!(stock.reset_request.is_none(), "hidden logic leaves no token for the stock field");
|
||||
}
|
||||
|
||||
/// Home suspends before input, so an interrupted commit allocates no reset and its old marker
|
||||
/// cannot rearm or leak into a stock field.
|
||||
#[test]
|
||||
fn home_with_an_interrupted_commit_allocates_no_reset_and_leaves_no_stock_token() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -573,6 +613,8 @@ fn home_with_an_interrupted_commit_allocates_no_reset_and_leaves_no_stock_token(
|
||||
assert!(crate::ime::pending_request(&app, &ctx).is_none(), "the old marker cannot rearm a retired fence");
|
||||
}
|
||||
|
||||
/// A command batch that opens the palette allocates no speculative reset; the palette's stock field
|
||||
/// receives no custom token.
|
||||
#[test]
|
||||
fn command_palette_opened_by_a_commit_batch_gets_no_speculative_reset() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -598,6 +640,8 @@ fn command_palette_opened_by_a_commit_batch_gets_no_speculative_reset() {
|
||||
assert_eq!(plain(&app, id), "axbcdé", "the application text lands at the fenced caret");
|
||||
}
|
||||
|
||||
/// Presses that end on an invalid destination release no custom descriptor and allocate no reset
|
||||
/// for the stock field.
|
||||
#[test]
|
||||
fn multiple_pointer_presses_ending_on_an_invalid_destination_leave_no_stock_token() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -627,6 +671,8 @@ fn multiple_pointer_presses_ending_on_an_invalid_destination_leave_no_stock_toke
|
||||
assert!(stock.reset_request.is_none(), "the invalidated final pointer leaves no stock token");
|
||||
}
|
||||
|
||||
/// Cmd+X and Cmd+V in a deferred command batch are dropped until the replacement fence, leaving the
|
||||
/// text and undo history unchanged.
|
||||
#[test]
|
||||
fn deferred_command_key_forms_drop_cmd_x_and_cmd_v_until_the_fence() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -655,6 +701,8 @@ fn deferred_command_key_forms_drop_cmd_x_and_cmd_v_until_the_fence() {
|
||||
assert_ne!(first, second);
|
||||
}
|
||||
|
||||
/// A live composition alone requests no reset; a stock field publishes no custom token, and
|
||||
/// returning to Type accepts fresh composition.
|
||||
#[test]
|
||||
fn a_stock_field_with_an_active_composition_but_no_guard_gets_no_reset_request() {
|
||||
let (mut app, ctx, id, _) = interruption_editor();
|
||||
@@ -687,6 +735,8 @@ fn a_stock_field_with_an_active_composition_but_no_guard_gets_no_reset_request()
|
||||
assert_eq!(plain(&app, id), "aがbcdé", "a clear adapter accepts fresh composition without a stale latch");
|
||||
}
|
||||
|
||||
/// Opening a modal after the canvas suspends the fence; returning to the same caret allocates a new
|
||||
/// fence and old input stays stale.
|
||||
#[test]
|
||||
fn a_modal_dialog_suspends_a_live_fence_and_the_same_caret_rearms() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -711,6 +761,8 @@ fn a_modal_dialog_suspends_a_live_fence_and_the_same_caret_rearms() {
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// An absent pass starts a new context generation for the same owner and publishes a replacement
|
||||
/// fence on return.
|
||||
#[test]
|
||||
fn a_same_owner_absent_pass_gets_a_new_generation_and_replacement_fence() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -731,6 +783,8 @@ fn a_same_owner_absent_pass_gets_a_new_generation_and_replacement_fence() {
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// An occluded pass leaves the marker inert and keeps the guard suspended until a visible pass re-
|
||||
/// arms it.
|
||||
#[test]
|
||||
fn geometry_loss_before_a_marker_keeps_the_fence_suspended_until_reactivation() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -757,6 +811,8 @@ fn geometry_loss_before_a_marker_keeps_the_fence_suspended_until_reactivation()
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// Settling a preview through an application selection change allocates exactly one fence for the
|
||||
/// post-settlement destination, with no preflight retry.
|
||||
#[test]
|
||||
fn settling_a_preview_without_a_pointer_allocates_one_request_after_the_commit() {
|
||||
let (mut app, ctx, id, _) = interruption_editor();
|
||||
@@ -778,6 +834,8 @@ fn settling_a_preview_without_a_pointer_allocates_one_request_after_the_commit()
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// Marked text left without a recorded composition triggers an interrupting descriptor whose
|
||||
/// request-bound fence admits only fresh input.
|
||||
#[test]
|
||||
fn an_untokened_marked_text_interruption_binds_a_request() {
|
||||
let (mut app, ctx, id, _) = interruption_editor();
|
||||
@@ -793,6 +851,8 @@ fn an_untokened_marked_text_interruption_binds_a_request() {
|
||||
assert_eq!(plain(&app, id), "abcdが");
|
||||
}
|
||||
|
||||
/// Logic inside a full UI pass preserves the fence; repeated standalone logic keeps it blocked and
|
||||
/// the first UI return re-arms once.
|
||||
#[test]
|
||||
fn full_pass_logic_keeps_its_fence_but_standalone_logic_rearms_once() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -834,6 +894,8 @@ fn full_pass_logic_keeps_its_fence_but_standalone_logic_rearms_once() {
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// An application edit that preserves the caret but changes committed text invalidates the fence
|
||||
/// before its raw suffix runs.
|
||||
#[test]
|
||||
fn same_caret_external_edit_invalidates_a_fence_before_its_raw_suffix() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -858,6 +920,8 @@ fn same_caret_external_edit_invalidates_a_fence_before_its_raw_suffix() {
|
||||
assert_eq!(plain(&app, id), "Aがbcdé");
|
||||
}
|
||||
|
||||
/// An auxiliary viewport observes only its own guard; a replacement egui context rejects the old
|
||||
/// request and publishes a new one.
|
||||
#[test]
|
||||
fn a_replaced_context_and_auxiliary_ui_cannot_reuse_the_root_fence() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -895,6 +959,8 @@ fn a_replaced_context_and_auxiliary_ui_cannot_reuse_the_root_fence() {
|
||||
assert_eq!(plain(&app, id), "aがbcdé");
|
||||
}
|
||||
|
||||
/// A hidden logic pass neither interrupts an intact composition nor retries acknowledgement; non-
|
||||
/// empty and old-terminal input stays rejected.
|
||||
#[test]
|
||||
fn hidden_logic_preserves_an_uninterrupted_composition_and_no_ack_stays_closed() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -917,6 +983,8 @@ fn hidden_logic_preserves_an_uninterrupted_composition_and_no_ack_stays_closed()
|
||||
}
|
||||
}
|
||||
|
||||
/// A real pointer interruption rejects following-batch IME deletes and commits without previewing
|
||||
/// them, then admits fresh input only after its fence.
|
||||
#[test]
|
||||
fn pointer_interruption_rejects_next_batch_ime_mutations_and_allows_fresh_input() {
|
||||
for bytes in [false, true] {
|
||||
@@ -949,6 +1017,8 @@ fn pointer_interruption_rejects_next_batch_ime_mutations_and_allows_fresh_input(
|
||||
}
|
||||
}
|
||||
|
||||
/// Late composition input after clicking another text object cannot mutate either object; the
|
||||
/// replacement target's fence admits only its own commit.
|
||||
#[test]
|
||||
fn target_switch_rejects_late_composition_input_before_mutating_the_new_text() {
|
||||
for bytes in [false, true] {
|
||||
@@ -986,6 +1056,8 @@ fn target_switch_rejects_late_composition_input_before_mutating_the_new_text() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A composition's own delete plus commit, and a non-composing pointer commit, remain accepted
|
||||
/// single undoable edits.
|
||||
#[test]
|
||||
fn valid_composition_delete_commit_and_noncomposing_pointer_commit_stay_accepted() {
|
||||
for bytes in [false, true] {
|
||||
@@ -1010,6 +1082,8 @@ fn valid_composition_delete_commit_and_noncomposing_pointer_commit_stay_accepted
|
||||
assert_eq!(plain(&app, id), "abcd");
|
||||
}
|
||||
|
||||
/// A stale non-empty pre-edit stays rejected across idle frames until its matching fence; only
|
||||
/// post-fence input changes text.
|
||||
#[test]
|
||||
fn a_stale_nonempty_preedit_stays_rejected_until_its_matching_fence() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -1034,6 +1108,8 @@ fn a_stale_nonempty_preedit_stays_rejected_until_its_matching_fence() {
|
||||
assert_eq!(plain(&app, id), text);
|
||||
}
|
||||
|
||||
/// An application caret change settles the preview at its old position and rejects old input
|
||||
/// without changing the document.
|
||||
#[test]
|
||||
fn an_external_caret_change_keeps_preview_at_its_old_position_and_rejects_old_input() {
|
||||
let (mut app, ctx, id, _) = interruption_editor();
|
||||
@@ -1051,6 +1127,8 @@ fn an_external_caret_change_keeps_preview_at_its_old_position_and_rejects_old_in
|
||||
assert_eq!(undo(&app), steps);
|
||||
}
|
||||
|
||||
/// An application edit while typing makes the payload and following commit `Other` and publishes
|
||||
/// the document's current text, not the stale session.
|
||||
#[test]
|
||||
fn external_edit_while_typing_is_other_and_ime_commit_uses_the_document_text() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1083,6 +1161,7 @@ fn external_edit_while_typing_is_other_and_ime_commit_uses_the_document_text() {
|
||||
assert_eq!((payload.text.as_str(), payload.cursor, payload.anchor), ("XYZy", 4, 4));
|
||||
}
|
||||
|
||||
/// A dialog opened after the canvas takes IME ownership away from the Type tool on that pass.
|
||||
#[test]
|
||||
fn a_dialog_opened_after_the_canvas_takes_the_ime_away_from_the_type_tool() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1103,6 +1182,8 @@ fn a_dialog_opened_after_the_canvas_takes_the_ime_away_from_the_type_tool() {
|
||||
assert!(during.is_none(), "the canvas must not own the IME while a dialog opened after it: {during:?}");
|
||||
}
|
||||
|
||||
/// Home removes the Type tool owner, clears marked text and asks the system IME to discard its
|
||||
/// composition.
|
||||
#[test]
|
||||
fn home_takes_down_a_live_composition() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1121,6 +1202,8 @@ fn home_takes_down_a_live_composition() {
|
||||
assert!(!app.session.tool_composing(), "no stale composition survives Home");
|
||||
}
|
||||
|
||||
/// Closing the active document removes the IME owner, clears marked text and asks the system IME to
|
||||
/// discard its composition.
|
||||
#[test]
|
||||
fn closing_the_document_takes_down_a_live_composition() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1137,6 +1220,8 @@ fn closing_the_document_takes_down_a_live_composition() {
|
||||
assert!(app.take_ime_discard(), "the system IME is told to drop its marked text");
|
||||
}
|
||||
|
||||
/// A commit whose text the document rewrites through smart quotes or control filtering is reported
|
||||
/// as `Other`, not `InputMethod`.
|
||||
#[test]
|
||||
fn an_ime_commit_the_document_rewrites_is_other() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1158,6 +1243,8 @@ fn an_ime_commit_the_document_rewrites_is_other() {
|
||||
assert_eq!(filtered.change_cause, egui::output::ImeChangeCause::Other, "filtering is not an IME insertion");
|
||||
}
|
||||
|
||||
/// Smart-quote replacement examines the character before the pre-edit, so the rewritten commit is
|
||||
/// reported as `Other`.
|
||||
#[test]
|
||||
fn an_ime_quote_commit_uses_the_character_before_its_pre_edit() {
|
||||
let (mut app, id) = app_with_text("a");
|
||||
@@ -1215,6 +1302,8 @@ fn ime_delete_surrounding_after_end_typing_keeps_the_next_commit_input_method()
|
||||
assert_eq!(committed.change_cause, egui::output::ImeChangeCause::InputMethod, "a caret move is not an application-side reset");
|
||||
}
|
||||
|
||||
/// Reloading a document keeps its uid and text-node id but changes the edit-session replacement
|
||||
/// epoch.
|
||||
#[test]
|
||||
fn a_reloaded_document_gets_a_new_ime_replacement_epoch() {
|
||||
let (mut app, id) = app_with_text("Same");
|
||||
@@ -1372,10 +1461,12 @@ fn an_unavailable_ime_boundary_carries_the_document_revision() {
|
||||
assert!(after.unavailable_revision().is_some_and(|after| after > revision), "the document revision advanced");
|
||||
}
|
||||
|
||||
/// Build a primary-pointer button event at `at` with the requested press state.
|
||||
fn primary_button(at: egui::Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
/// Build a left-arrow key event marked as a physical key press or release.
|
||||
fn left_key(pressed: bool) -> Event {
|
||||
Event::Key { key: egui::Key::ArrowLeft, physical_key: Some(egui::Key::ArrowLeft), pressed, repeat: false, modifiers: Default::default() }
|
||||
}
|
||||
@@ -1432,6 +1523,8 @@ fn a_pointer_before_same_batch_terminal_input_preserves_the_interrupted_text() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Application navigation before a same-batch pending terminal is applied once; the old
|
||||
/// delete/commit suffix is rejected and only input after the fence is admitted.
|
||||
#[test]
|
||||
fn application_navigation_before_same_batch_pending_terminal_rejects_its_mutations() {
|
||||
for bytes in [false, true] {
|
||||
@@ -1485,6 +1578,8 @@ fn terminal_before_a_same_batch_pointer_is_accepted_in_both_composition_states()
|
||||
}
|
||||
}
|
||||
|
||||
/// A terminal followed by application navigation keeps the valid commit and runs the navigation; an
|
||||
/// active IME's own navigation does not interrupt it.
|
||||
#[test]
|
||||
fn terminal_before_navigation_is_accepted_and_active_ime_navigation_stays_owned() {
|
||||
for bytes in [false, true] {
|
||||
@@ -1505,6 +1600,8 @@ fn terminal_before_navigation_is_accepted_and_active_ime_navigation_stays_owned(
|
||||
}
|
||||
}
|
||||
|
||||
/// A pointer marker without an observed old composition does not reject a direct commit; terminal
|
||||
/// text still executes before pointer placement.
|
||||
#[test]
|
||||
fn a_pointer_marker_without_an_old_composition_does_not_reject_a_direct_commit() {
|
||||
let (mut app, ctx, id, point) = interruption_editor();
|
||||
@@ -1517,6 +1614,8 @@ fn a_pointer_marker_without_an_old_composition_does_not_reject_a_direct_commit()
|
||||
assert_eq!(plain(&app, id), "abcd");
|
||||
}
|
||||
|
||||
/// A move within an already-held gesture before a same-batch terminal interrupts the old
|
||||
/// composition; the retained move reaches the tool and only the live preview commits.
|
||||
#[test]
|
||||
fn a_held_pointer_move_before_same_batch_terminal_also_interrupts_the_old_composition() {
|
||||
for bytes in [false, true] {
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::{Services, VectorcraftApp};
|
||||
|
||||
const SCREEN: egui::Vec2 = vec2(320.0, 200.0);
|
||||
|
||||
/// Build a screenshot test app with a write service, control channel and temporary directory on a
|
||||
/// 100 by 100 document.
|
||||
fn app(tag: &str) -> (VectorcraftApp, std::sync::mpsc::Sender<ControlRequest>, std::path::PathBuf) {
|
||||
let dir = vectorcraft_testkit::temp_dir(tag);
|
||||
let services =
|
||||
@@ -24,12 +26,14 @@ fn app(tag: &str) -> (VectorcraftApp, std::sync::mpsc::Sender<ControlRequest>, s
|
||||
(app, tx, dir)
|
||||
}
|
||||
|
||||
/// Submit a `ui.screenshot` control request and return its reply receiver.
|
||||
fn request(tx: &std::sync::mpsc::Sender<ControlRequest>, params: serde_json::Value) -> std::sync::mpsc::Receiver<serde_json::Value> {
|
||||
let (req, reply) = ControlRequest::new("ui.screenshot", params);
|
||||
tx.send(req).unwrap();
|
||||
reply
|
||||
}
|
||||
|
||||
/// Run one tick-then-UI host frame at the requested screen size and discard texture deltas.
|
||||
fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, size: egui::Vec2, events: Vec<Event>) -> egui::FullOutput {
|
||||
let raw = egui::RawInput { events, screen_rect: Some(Rect::from_min_size(Pos2::ZERO, size)), ..Default::default() };
|
||||
app.begin_host_iteration();
|
||||
@@ -41,6 +45,7 @@ fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, size: egui::Vec2, eve
|
||||
out
|
||||
}
|
||||
|
||||
/// Run one host frame at the standard 320 by 200 screenshot size.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context) -> egui::FullOutput {
|
||||
frame_at(app, ctx, SCREEN, vec![])
|
||||
}
|
||||
@@ -85,6 +90,8 @@ fn admit(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
allow_due(app);
|
||||
}
|
||||
|
||||
/// One admitted capture completes once, writes the named file, replies with its size and releases
|
||||
/// the reservation.
|
||||
#[test]
|
||||
fn a_request_saves_one_file_and_replies_once() {
|
||||
let (mut app, tx, dir) = app("screenshot-one");
|
||||
@@ -108,6 +115,7 @@ fn a_request_saves_one_file_and_replies_once() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A timed-out request replies once and writes nothing; a later completion is inert.
|
||||
#[test]
|
||||
fn a_timed_out_request_writes_nothing_and_a_late_completion_is_inert() {
|
||||
let (mut app, tx, dir) = app("screenshot-timeout");
|
||||
@@ -133,6 +141,8 @@ fn a_timed_out_request_writes_nothing_and_a_late_completion_is_inert() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A completion collected after the deadline is neither encoded nor written and replies once with
|
||||
/// the no-frame error.
|
||||
#[test]
|
||||
fn a_completion_that_arrives_after_the_deadline_is_not_saved() {
|
||||
let (mut app, tx, dir) = app("screenshot-deadline");
|
||||
@@ -157,6 +167,8 @@ fn a_completion_that_arrives_after_the_deadline_is_not_saved() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// Admission refuses a fifth request without evicting the four pending ones and releases capacity
|
||||
/// when one completes.
|
||||
#[test]
|
||||
fn admission_rejects_more_than_four_requests_without_evicting_one_and_releases_a_finished_one() {
|
||||
let (mut app, tx, dir) = app("screenshot-admission");
|
||||
@@ -188,6 +200,8 @@ fn admission_rejects_more_than_four_requests_without_evicting_one_and_releases_a
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// Pixels with a different size are stale, produce no reply or file, and a retry with the matching
|
||||
/// image completes the request.
|
||||
#[test]
|
||||
fn an_image_of_another_size_is_stale_and_retried() {
|
||||
let (mut app, tx, dir) = app("screenshot-size");
|
||||
@@ -213,6 +227,8 @@ fn an_image_of_another_size_is_stale_and_retried() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// Changing and restoring the DPI still invalidates in-flight pixels by epoch; the retry for the
|
||||
/// current geometry succeeds.
|
||||
#[test]
|
||||
fn a_geometry_change_and_change_back_still_rejects_the_old_pixels() {
|
||||
let (mut app, tx, dir) = app("screenshot-epoch");
|
||||
@@ -243,6 +259,8 @@ fn a_geometry_change_and_change_back_still_rejects_the_old_pixels() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// Switching documents while a capture is in flight fails the request instead of saving new-
|
||||
/// document pixels.
|
||||
#[test]
|
||||
fn a_document_change_while_a_capture_is_in_flight_fails_instead_of_saving_it() {
|
||||
let (mut app, tx, dir) = app("screenshot-document");
|
||||
@@ -266,6 +284,8 @@ fn a_document_change_while_a_capture_is_in_flight_fails_instead_of_saving_it() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// Switching documents during settle fails at issuance without ever issuing a capture or keeping a
|
||||
/// reservation.
|
||||
#[test]
|
||||
fn a_document_change_during_the_settle_fails_at_issuance_without_issuing_a_capture() {
|
||||
let (mut app, tx, dir) = app("screenshot-document-settle");
|
||||
@@ -292,6 +312,7 @@ fn a_document_change_during_the_settle_fails_at_issuance_without_issuing_a_captu
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A second stale-sized completion answers the request once with an error and writes nothing.
|
||||
#[test]
|
||||
fn a_second_stale_completion_replies_once_with_an_error_and_writes_nothing() {
|
||||
let (mut app, tx, dir) = app("screenshot-stale-twice");
|
||||
@@ -315,6 +336,8 @@ fn a_second_stale_completion_replies_once_with_an_error_and_writes_nothing() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A hidden window's logic pass requests a hidden paint instead of issuing the queued capture; the
|
||||
/// capture remains pending.
|
||||
#[test]
|
||||
fn a_hidden_window_is_asked_to_paint_the_queued_capture() {
|
||||
let (mut app, tx, dir) = app("screenshot-hidden");
|
||||
@@ -336,6 +359,7 @@ fn a_hidden_window_is_asked_to_paint_the_queued_capture() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// An oversized claimed image is refused from the reply-encoding budget before any copy or encode.
|
||||
#[test]
|
||||
fn a_data_reply_over_the_encoding_budget_is_refused_before_encoding() {
|
||||
let (mut app, _tx, _dir) = app("screenshot-encoding");
|
||||
@@ -347,6 +371,8 @@ fn a_data_reply_over_the_encoding_budget_is_refused_before_encoding() {
|
||||
assert!(answer["error"].as_str().unwrap_or_default().contains("reply-encoding limit"), "{answer}");
|
||||
}
|
||||
|
||||
/// A path plus data capture writes the file and omits an oversized inline PNG; without a path the
|
||||
/// same request fails.
|
||||
#[test]
|
||||
fn a_path_plus_data_capture_writes_the_file_and_omits_an_oversized_inline_png() {
|
||||
let (mut app, _tx, dir) = app("screenshot-path-data");
|
||||
|
||||
@@ -16,12 +16,15 @@ use crate::{icons, scrub};
|
||||
struct HashOnly<T>(T);
|
||||
|
||||
impl<T: std::hash::Hash> std::hash::Hash for HashOnly<T> {
|
||||
/// Hash only the wrapped id salt, deliberately omitting the debug-only identity that egui 0.37's
|
||||
/// `AsIdSalt` requires.
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for HashOnly<T> {
|
||||
/// Format any `HashOnly` salt as the fixed text `widget id` so ids do not expose their value.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("widget id")
|
||||
}
|
||||
@@ -246,6 +249,8 @@ struct NextLabel {
|
||||
next_auto_id: egui::Id,
|
||||
}
|
||||
|
||||
/// Record a label's widget id, viewport, pass, position and next auto id so only the immediately
|
||||
/// following field can take the label.
|
||||
fn remember_next_label(ui: &Ui, id: egui::Id) {
|
||||
let label = NextLabel {
|
||||
id,
|
||||
@@ -3579,6 +3584,7 @@ pub(crate) mod look_tests {
|
||||
assert!(lit(&hovered, row), "the row under the pointer is lit: {row:?}");
|
||||
}
|
||||
|
||||
/// Each painted row label names its own numeric field rather than the field's internal salt.
|
||||
#[test]
|
||||
fn a_label_rows_input_is_named_by_the_row_label() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
@@ -3599,6 +3605,7 @@ pub(crate) mod look_tests {
|
||||
assert!(h.query_by_role_and_label(egui::accesskit::Role::TextInput, "plugin-angle").is_none(), "no internal widget id");
|
||||
}
|
||||
|
||||
/// A label followed by a checkbox or other widget does not name a later field.
|
||||
#[test]
|
||||
fn a_label_does_not_name_a_field_after_another_widget() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
@@ -3655,6 +3662,7 @@ pub(crate) mod look_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Enter and focus guard's reconstructed id equals the id egui gives the ComboBox button.
|
||||
#[test]
|
||||
fn a_combos_keyboard_guard_reads_the_combos_own_button_id() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
# Development
|
||||
|
||||
> **Last updated:** 2026-10-25 · **Change:** Linux Wayland drag-and-drop note names eframe 0.37 (still on winit 0.30).
|
||||
> **Last updated:** 2026-10-23 · **Change:** Tests: cargo xtask test, one integration-test binary per crate, the dev profile's debug info.
|
||||
> **Last updated:** 2026-10-21 · **Change:** the interface faces as vectorcraft-ui-egui's `interface-fonts` feature, and `theme::install_fonts` for an embedder that draws the panels itself.
|
||||
> **Last updated:** 2026-10-21 · **Change:** leaving out the bundled fonts with the egui panels (vectorcraft-ui-egui's and vectorcraft-mcp's `bundled-fonts` features).
|
||||
@@ -115,7 +116,7 @@ The window runs natively on Wayland (eframe's `wayland` feature) and on X11. On
|
||||
- **Clipboard:** under Wayland arboard talks to the compositor through the data-control protocol (wlroots compositors such as Sway and Hyprland, KDE Plasma), so bitmaps (`image/png`), SVG markup and text copied in any app paste (#398). Where the compositor lacks the protocol (GNOME's Mutter, [arboard#223](https://github.com/1Password/arboard/issues/223)), arboard falls back to the X11 clipboard through XWayland, which only holds what X11 apps copied. egui's own text paste into fields goes through smithay-clipboard and works either way.
|
||||
- **Clipboard owners that never answer (#510):** reading the clipboard waits on the app that owns it: arboard gives up on an X11 owner after 4 s, and a Wayland read waits for the owner to send its data. So on Linux the check that enables the Paste menu items runs on a background thread with a second clipboard handle (`crates/ui-egui/src/clipboard_probe.rs`, installed as `Services::clipboard_probe`), only while nothing is copied in the app and a document is open, and the window keeps drawing. A paste itself still reads on the UI thread, so pasting from such an owner waits for it once. Windows and macOS keep the check on the UI thread: Windows only asks which formats the clipboard holds, macOS asks the pasteboard server.
|
||||
- **Copied files:** files copied in a file manager (`text/uri-list`; `CF_HDROP` on Windows, file URLs on macOS) paste as the first one that is art (SVG, PDF, EPS, EMF/WMF or a bitmap; a `.ai` or `.ait` file from the editing data it holds, else as the PDF or PostScript it holds), before the clipboard's other formats (which include the files' paths as text).
|
||||
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.36 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
|
||||
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.37 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
|
||||
- **Pens and pen displays under Wayland (#491, #2028):** winit 0.30 doesn't bind the Wayland tablet protocol (`zwp_tablet_v2`; tablet input arrives in winit 0.31, [winit#4318](https://github.com/rust-windowing/winit/pull/4318), [egui#7731](https://github.com/emilk/egui/pull/7731)). Since Plasma 6.3, KWin no longer turns tablet input into pointer input for such apps ([kwin!6336](https://invent.kde.org/plasma/kwin/-/merge_requests/6336), commit [25c65abd](https://invent.kde.org/plasma/kwin/-/commit/25c65abddd95eb87fb892a8594faccbf509ff62f)), and GNOME's Mutter does not turn it into pointer input either (that commit's message): on KWin the pen moves the compositor's cursor over the window, but the app gets no motion or presses and shows none of its own cursors. Until eframe moves to winit 0.31, either run the app under XWayland, which supports the tablet protocol and hands the pen to X11 apps as a pointer (Preferences › Performance › Linux Display Server › X11 (XWayland); X11 (XWayland) When a Pen Is Attached (`x11WithPen`), which opens the window through XWayland only when `/proc/bus/input/devices` lists a pen as the app starts, a device with the `BTN_TOOL_PEN` key and the `ABS_X` and `ABS_Y` axes, and logs `pen found: opening the window through XWayland`; `WAYLAND_DISPLAY= vectorcraft`, or `env -u WAYLAND_DISPLAY vectorcraft`; for a desktop launcher, put `env WAYLAND_DISPLAY=` before the command in its `Exec=` line), or turn KWin's deprecated emulation back on for the whole session by setting `KWIN_WAYLAND_EMULATE_TABLET=1` in KWin's environment (for example in a file under `~/.config/environment.d/` or in `/etc/environment`) and logging in again. The pen then works as a mouse. Under XWayland the app also reads the pen's pressure (next item); with KWin's emulation on native Wayland the pen has no pressure. Other compositors that don't emulate a pointer for tablets behave the same way.
|
||||
- **Pen pressure under X11 and XWayland (#764):** winit 0.30 hands a pen to an X11 window as a mouse, without its pressure. When the window is on X11, XWayland included, the desktop app opens a second X connection (x11rb's `RustConnection`) and reads XInput2 raw events for every device (`apps/vectorcraft/src/tablet.rs`, from PhotoCraft's `crates/tablet`). A device with an `Abs Pressure` valuator is a pen, unless its XInput type is `TOUCHPAD` or `TOUCHSCREEN`. While a pen is the pointer that moved last, its pressure, scaled from the valuator's range to 0..1, reaches the tools that use pressure: the Paintbrush's pressure-sensitive brushes and the Liquify tools' Use Pressure Pen. A pen device whose name or XInput type contains `eraser` (in any case) is the pen's eraser end: while the pointer is over the window, the canvas selects the Eraser tool when the eraser end moves, and it selects the tool used before when the tip moves again (`crates/ui-egui/src/stylus.rs`). The X server needs XInput 2.1 or later; otherwise the log has a `tablet pressure:` warning with the reason, and the pen draws at full pressure.
|
||||
|
||||
@@ -490,6 +491,7 @@ Scores and hour estimates are unchanged.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | minor | Linux Wayland drag-and-drop note names eframe 0.37 (still on winit 0.30); behavior unchanged. |
|
||||
| 2026-10-23 | minor | Tests: `cargo xtask test` (nextest, then doctests), one integration-test binary per crate and the files that keep their own, the dev profile's debug info. |
|
||||
| 2026-10-21 | minor | Fonts › Leaving out the bundled fonts: vectorcraft-ui-egui's `interface-fonts` feature, and `theme::install_fonts` for an embedder that draws the panels without `VectorcraftApp::logic`. |
|
||||
| 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. |
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# VectorCraft — egui 0.37 migration plan
|
||||
|
||||
**Status (2026-10-25):** reconciled main `8bf090b25389b48aa6f9d7415de57c9ce7ccf8cb` and passed all 12 mandatory CI stages. Managed job `egui037-vectorcraft-artboard-green-ci-20261025-0589` recorded 10,197 passing test executions, zero failures and 14 ignored. This includes the artboard-name geometry regression, demonstrated failing before the explicit inset repair; input/IME fixtures, no-bundled-font checks and the web app build passed. Native/platform GUI and IME workflows, final packages and paired app performance remain open. Earlier source identities and counts below are historical.
|
||||
**Status (2026-10-25, shared-docking reconciliation):** the published qualification remains checkpoint `83d6dd2d6603845978f949523e6da2b4945dcffb`, tested with post-PR-4113 `main` `6e7854967d6b8a094bfed809643bd957aa3ba66f` and the two `Id::unique` edits at `crates/ui-egui/src/dialogs/recolor.rs:384,725`; managed job `egui037-vector-postmerge-ci-20261025-1381` passed all 12 mandatory CI stages for that working tree, and publication commit `1072d0e263623882da68d8115934630abbef33ae` records it in PR #4173. The reviewed recolor code is already in `main`: `HEAD` and `origin/main` resolve the same blob `6050af15a3bf7b9a056df6dff347bda7690ad251` for that file.
|
||||
|
||||
This checkout, at documentation-only follow-up `12fee85c9cbf9f7f1609a53bc67327b12e4f4dd1` (`M14: distinguish qualification from publication`), is locally reconciling upstream `main` `714434527e1a3e03c4b49096e30dd92b258aab50` (PR #4135: panels on craft-ui's shared docking, floating panels in OS windows). The four conflicted files were resolved by taking upstream's `dock.rs`, `panel_docking.rs` and `panel_docking_tests.rs` and deleting `floating.rs`; `git diff` against `714434527…` is empty for those three files, and no `crate::floating` or `mod floating` path remains. Managed CI job 1422 passed all 12 mandatory stages on the pre-merge tree (`12fee85c9…`); managed job `egui037-vector-combined-ci1443` subsequently passed all 12 mandatory CI stages on the combined working tree, including WASM. Independent integration reviews accepted the conflict resolutions and shared-library pins. This qualification covers the executable source and lockfile; the qualification prose was updated afterward to record that result. Native/platform GUI and IME workflows, final packages and paired app performance remain open.
|
||||
|
||||
**Historical status (superseded, not current acceptance):** the earlier reconciliation against `main` `8bf090b25389b48aa6f9d7415de57c9ce7ccf8cb` and managed job `egui037-vectorcraft-artboard-green-ci-20261025-0589` recorded 10,197 passing test executions, zero failures and 14 ignored, including the artboard-name geometry regression, input/IME fixtures, no-bundled-font checks and the web app build. The 0.36.2 lock table and the plan-time local craft-libs/CL-2F statements below are historical planning state. Current dependency pins (read from this checkout's `Cargo.toml` and `Cargo.lock` on 2026-10-25): `egui`, `eframe`, `egui-winit`, `egui-wgpu`, `egui_glow`, `egui_extras`, `egui_kittest`, `epaint`, `emath`, `ecolor`, `epaint_default_fonts` are `0.37.0` from `storytold/egui` rev `6d518fc799681571963842f95b55a6b68431b15a`; `egui_system_font_provider` has an unused patch at that same revision and is not in the resolved dependency graph; `winit` is `0.30.13` from `storytold/winit` rev `a517b3846ee8457d84ee4975110bfda6797c44dc`; `wgpu` is `30.0.1`; `kittest` is `0.5.0`; `accesskit`/`accesskit_winit` are `0.25.1`/`0.34.1`; `craft-ui`, `craft-window`, `craft-keymap`, `craft-print`, `craft-pdf` and `craft-docpath` use published `storytold/craft-libs` rev `a0916808f7d78bed8c7b90cdf937176015454a7f`, as do the `hayro-*` and `krilla` patches.
|
||||
|
||||
**Earlier status (historical):** implementation revision 4, 2026-10-24. The VectorCraft migration is implemented in the
|
||||
`vectorcraft-implementation` worktree; this revision incorporates the final upstream-review repairs. The exact
|
||||
current tree still needs a fresh managed build/test/lint run before publication; earlier managed evidence
|
||||
(`tests-12`, `lints-13`) is not claimed for these later edits. Every claim below remains source-grounded in the
|
||||
`vectorcraft-implementation` worktree; this revision incorporates the final upstream-review repairs. At revision 4
|
||||
the exact tree still needed a fresh managed build/test/lint run before publication; earlier managed evidence
|
||||
(`tests-12`, `lints-13`) is not claimed for those later edits. Every claim below remains source-grounded in the
|
||||
checkouts named here; historical compiler diagnostics and reviewer findings are clues, not acceptance evidence.
|
||||
|
||||
**Assigned source:** `/home/codex/work/CraftApps/egui037-migration-20261022/vectorcraft-implementation` — the
|
||||
@@ -24,6 +28,15 @@ implementation worktree. The earlier pinned review checkout was `vectorcraft` at
|
||||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/remote-plan-bases-20261023-0632.json`.
|
||||
**Clean-room note:** this plan uses only public specifications, public documentation and source code. It does not use, inspect or compare against any Adobe application, asset or output.
|
||||
|
||||
### Shared-docking integration (current, 2026-10-25)
|
||||
|
||||
This subsection records the current code after PR #4135; §§2–5 below remain the original planning record and are marked historical where the merged code replaced them.
|
||||
|
||||
- **Desktop versus web/Wayland:** on the desktop, floating panels open in OS windows of their own through craft-ui's target-gated `native-windows` feature (`crates/ui-egui/Cargo.toml`, `crates/ui-egui/src/panel_docking.rs` module docs, `apps/vectorcraft/src/main.rs::native_input_hook`). On the web and on Wayland they float inside the app window (`panel_docking.rs`; `docs/control-protocol.md`).
|
||||
- **Removed app-side floating:** `crates/ui-egui/src/floating.rs` is deleted. `dock.rs` is now 137 lines and holds `panel_body` plus two scroll-bar tests; `panel_docking.rs` draws the canvas and panels through `craft_ui::docking::DockArea`, with the session-only `Runtime`, `inspect`, `hit_rects`, `known` and `valid`. The old `apply_pending_dock_width`, `cancel_legacy_drag`, `seed_dock_panel_width`, `note_dock_width`, `legacy_dock_default_width`, `shared_dock_default_width` and `dock_width_pending` symbols are gone. `tools_float.rs` keeps the Tools panel's own move state.
|
||||
- **Dock-width persistence:** `UiState::dock_width` is the app-owned preference. `panel_docking::main_width` clamps it into the default layout, `default_flags`/`Transaction::commit` writes a resized width back, `workspaces::Workspace` captures and applies it (test `saved_workspaces_restore_dock_width`), and `layout_migration` carries it across older preference files. No `PanelState` seeding is used for dock width.
|
||||
- **Tests:** `panel_docking_tests.rs` covers pointer/keyboard docking; `panel_docking_native_tests.rs` covers desktop OS windows (keyboard, drops, modal focus and `ui.docking` hit-rect filtering); `layout_migration_tests.rs` covers conversion of layouts saved before the canvas joined the shared dock; `dock.rs` retains two scroll-bar accessibility tests; `workspaces.rs` covers saved-width restore. These test surfaces are included in the combined-tree CI qualification recorded above; native GUI workflows remain a separate open gate.
|
||||
|
||||
This plan is deliberately app-specific. The suite-wide dependency order and shared acceptance contract are in the
|
||||
local planning draft `craft-libs/docs/egui-037-suite-rollout.md` (not yet published at the time of writing); this
|
||||
document only records what VectorCraft must do, what it must prove and what remains unresolved. The shared
|
||||
@@ -34,8 +47,8 @@ lifecycle, capture-slot, performance and font-fixture contracts in that rollout
|
||||
|
||||
## 1. Executive decision
|
||||
|
||||
Migrate VectorCraft from the coherent egui **0.36.2** family (currently resolved from crates.io, with `craft-ui` and
|
||||
`craft-window` pinned to 0.36-era craft-libs revisions) to the selected **egui 0.37.0** family and the suite's
|
||||
Migrate VectorCraft from its pre-migration coherent egui **0.36.2** family (then resolved from crates.io, with
|
||||
`craft-ui` and `craft-window` pinned to 0.36-era craft-libs revisions) to the selected **egui 0.37.0** family and the suite's
|
||||
`winit 0.30.13` language fork, in a sequence that never leaves `main` with a mixed or unbuildable dependency graph.
|
||||
|
||||
The migration is not a version-string bump:
|
||||
@@ -94,7 +107,7 @@ publishing a mixed or reduced graph.
|
||||
| storytold egui 0.36.2 | `767b1cad294120689a514bb4bd68c077e46679b9` (`craft/0.36.2`) | Existing published fork for comparison |
|
||||
| aiongg winit candidate | `8b35388bd719e8cc86a95971ba0bc026c9a8f058` (`aiongg/winit craft/0.30.13-lang`) | **Research input only**, not a production pin; missing W15P and W27 lifecycle revisions |
|
||||
| Published storytold winit | `ffae5198770d7730e718043f035a772b7dbc3b1b` (`craft/0.30.13`) | Old coherent comparator |
|
||||
| craft-libs implementation base | `3832c47ef68a0e6214f2c1e85f3526b6adcb686e` | Current local shared-library tree and the base for the implemented port; it already carries PR 199's egui 0.37 update, while the CL-2F capture helper remains uncommitted and is a publication gate |
|
||||
| craft-libs planning base (historical) | `3832c47ef68a0e6214f2c1e85f3526b6adcb686e` | Local shared-library tree used to prepare the port; it carries PR 199's egui 0.37 update. It is not the current pin. The current workspace uses published craft-libs `a0916808f7d78bed8c7b90cdf937176015454a7f`, which includes the CL-2F capture helper |
|
||||
| craft-libs PR 199 head | `b68de8c5f4f1c060904d211a7b1320aa0f7bce78` | **Stale tree; do not merge.** Reuse selected port hunks only (§3.7) |
|
||||
| craft-libs PR 199 API commit | `8614e4a04fae516fba16c22b31ff1008cb6be10b` | Historical source of the egui API-port hunks already contained in the `3832c47…` base |
|
||||
| craft-libs PR 199 README commit | `1764bca` | README/`scope_id` example hunk to review and apply separately |
|
||||
@@ -103,7 +116,7 @@ publishing a mixed or reduced graph.
|
||||
| Adversarial plan review, revision 2 | `plan-reviews/vectorcraft-v2.md` | Reviewed document `d8c6a7594015919751d2b83bbe0e44ee9fdb4696b4e5c90fb1f8ecbdf7335f83`; findings resolved in §8.2 |
|
||||
| Remote bases snapshot | `remote-plan-bases-20261023-0632.json` | Records all suite heads and the two-commit VectorCraft drift above |
|
||||
|
||||
Current VectorCraft lock identities (from `Cargo.lock`):
|
||||
Historical pre-migration VectorCraft lock identities (from `Cargo.lock` at the pinned `890607…` baseline; the 0.36 comparison arm, not the current lock):
|
||||
|
||||
| Package | Version | Source |
|
||||
|---|---|---|
|
||||
@@ -144,12 +157,15 @@ Root workspace (`Cargo.toml`):
|
||||
|
||||
- `resolver = "3"`, members `crates/*`, `apps/*`, `xtask`; default members include `apps/vectorcraft`,
|
||||
`apps/vectorcraft-cli` and `xtask`.
|
||||
- Workspace UI dependencies at lines 102–105: `egui = "0.36"`, `egui_extras = { version = "0.36",
|
||||
default-features = false, features = ["svg"] }`, `eframe = { version = "0.36", default-features = false }`,
|
||||
`egui_kittest = "0.36"`.
|
||||
- `craft-ui` rev `5c874d…` with `serde`; `craft-window` rev `1f805…` with `egui`.
|
||||
- Root `[patch.crates-io]` currently patches `hayro-syntax`, `hayro-interpret`, `hayro-jbig2` and `krilla` to the
|
||||
same craft-libs rev `ca50d63b…`. These are unrelated to egui and must remain unless a separate comparison proves
|
||||
- Current workspace UI dependencies: `egui = "0.37"`, `egui_extras = { version = "0.37",
|
||||
default-features = false, features = ["svg"] }`, `eframe = { version = "0.37", default-features = false }`,
|
||||
`egui_kittest = "0.37"`.
|
||||
- Current `craft-ui`, `craft-window` and `craft-keymap` pins are published craft-libs rev `a0916808…`
|
||||
(`craft-ui` with `serde` and target-gated `native-windows`; `craft-window` with `egui`); `craft-print`,
|
||||
`craft-pdf` and `craft-docpath` use the same revision.
|
||||
- Root `[patch.crates-io]` maps the egui family (including `eframe` and `egui_kittest`) to `storytold/egui` rev
|
||||
`6d518fc7…`, `winit` to `storytold/winit` rev `a517b384…`, and `hayro-*`/`krilla` to craft-libs rev
|
||||
`a0916808…`. The hayro/krilla patches are unrelated to egui and must remain unless a separate comparison proves
|
||||
replacement.
|
||||
- `vendor/windows-link` is the only vendored package. `vendor/windows-link/VECTORCRAFT-PATCH.md` records the
|
||||
provenance (Microsoft windows-link 0.2.1, crates.io, MIT OR Apache-2.0), the sole change (`CoTaskMemFree` from
|
||||
@@ -160,9 +176,9 @@ Root workspace (`Cargo.toml`):
|
||||
example; no egui). `crates/plugins/example/Cargo.lock` is separate and must not acquire egui.
|
||||
- `crates/affinity/fuzz` has no lockfile in the checkout. Do not create one merely for this migration.
|
||||
|
||||
Manifests that participate in the egui graph:
|
||||
Manifests that participate in the egui graph (the table records the plan-time declarations and required changes; current declarations are listed above):
|
||||
|
||||
| Manifest | Current UI declaration | Required change |
|
||||
| Manifest | Plan-time UI declaration | Required change |
|
||||
|---|---|---|
|
||||
| `crates/ui-egui/Cargo.toml` | `egui` workspace + `bytemuck`; `egui_extras`; dev `egui_kittest` `wgpu`,`snapshot` | `egui` must also enable `default_fonts` for library tests/embedders and for downstream kittest tests, which do not inherit kittest's own dev-dependency fonts; `egui_extras`/`egui_kittest` to 0.37; no feature rename assumed |
|
||||
| `apps/vectorcraft/Cargo.toml` | `eframe` features `default_fonts`, `wayland`, `x11`; default features `wgpu`, `accessibility`; `windows7 = ["eframe/glow"]`; direct `winit 0.30` | add **`eframe/clipboard`**; keep `links` off (existing native `open_url` service); keep `system_fonts` off; retain winit feature set |
|
||||
@@ -205,15 +221,15 @@ Entry-point details that must remain true:
|
||||
- The canvas is CPU-rasterized by `vectorcraft_render` and composited by wgpu; egui only uploads the raster texture.
|
||||
UI-frame work and document rendering are separate measurements.
|
||||
|
||||
### 2.4 Current lifecycle (the facts, not the desired design)
|
||||
### 2.4 Historical 0.36.2 lifecycle (planning snapshot, not current acceptance)
|
||||
|
||||
The current app already has:
|
||||
The 0.36.2 baseline already had:
|
||||
|
||||
- `App::logic(&mut self, ctx, frame)` and `App::ui(&mut self, ui, frame)` in both hosts.
|
||||
- `VectorcraftApp::logic(ctx)` called from the host logic, guarded by `vectorcraft_engine::guard::catch_panic`.
|
||||
- `VectorcraftApp::ui(ui)` called from the host UI, also guarded.
|
||||
- A root `Ui` in `App::ui`; panels are already shown with `egui::Panel::…::show(ui, …)`.
|
||||
- Custom `Area`/`Window` dialogs and floating panels; no `show_viewport_deferred`/child native viewports.
|
||||
- Custom `Area`/`Window` dialogs and floating panels in the 0.36.2 baseline; at planning time the app had no `show_viewport_deferred`/child native viewports. The shared-docking integration now opens desktop floating panels as craft-ui OS windows, while the web and Wayland keep them in-page (see “Shared-docking integration (current)” above).
|
||||
- `ctx.run_ui` in many tests; `ctx.run_ui` in test helpers draws either the whole app or one panel/dialog.
|
||||
|
||||
What 0.37 makes explicit:
|
||||
@@ -252,7 +268,7 @@ numbers will drift; symbols are authoritative.
|
||||
| Class | Historical diagnostic examples | Source anchors in VectorCraft | Required migration |
|
||||
|---|---|---|---|
|
||||
| `Event::MouseWheel` gained `source: MouseWheelSource` | `canvas.rs:6226`, `chrome.rs:1691`, `stroke.rs:634`, tests | 21 `Event::MouseWheel` literals; production matches use `..` | Add `source: MouseWheelSource::Unknown` in tests/synthetic input; production events come from egui-winit. Preserve `unit`, `phase`, `delta`, `modifiers`. |
|
||||
| `WidgetType` is now a deprecated alias for `accesskit::Role`, and variants were renamed | `chrome.rs:640`, `dock.rs:122`, `edit_toolbar.rs:533`, `panel_docking.rs:1241`, `widgets.rs:192/1535/1536/2487` | 54 `WidgetType` call sites | Map per §3.2 to the renamed `Role` variants. Do not hide all roles behind `Role::Unknown`. |
|
||||
| `WidgetType` is now a deprecated alias for `accesskit::Role`, and variants were renamed | Historical anchors `chrome.rs:640`, `dock.rs:122`, `edit_toolbar.rs:533`, `panel_docking.rs:1241`, `widgets.rs:192/1535/1536/2487`; symbols are authoritative, not these line positions | 54 `WidgetType` call sites at planning time; the current tree has none | Map per §3.2 to the renamed `Role` variants. Do not hide all roles behind `Role::Unknown`. |
|
||||
| `Id` is not an `IdSalt` | `dialogs/layer_options.rs:129/130`, `menus.rs:3482/3484` | `Atom::custom(chip, …)`; `Panel::right(Id::new(...))`; `ComboBox::from_id_salt(id)` | `IdSalt::new(...)` only for a parameter typed exactly `IdSalt` (`Atom::custom`); pass the original source to `impl AsIdSalt` APIs (`Panel`, `ComboBox`, `ScrollArea`, `make_persistent_id`). `Id::unique(...)` for root ids. Never pass an `Id` as a salt. |
|
||||
| `WidgetInfo.typ` removed | `dialogs/tests_warp.rs:509` | one test | Use `info.role`. |
|
||||
| `scroll_area::State.offset` private | `font_menu.rs:507`, `panels/layers.rs:650`, `tests_numfields.rs:614` | 3 sites | Use the public `offset()` method. |
|
||||
@@ -297,16 +313,16 @@ names.
|
||||
- Exact `IdSalt` parameters (`Atom::custom`, `Id::with_salt`) take `IdSalt::new(source)`.
|
||||
- `Id` still satisfies the blanket `AsIdSalt` impl (`Hash + Debug`), so some wrong calls compile while hashing an
|
||||
`Id` as a salt. That is the wrong identity even when it compiles; `Atom::custom` rejects `Id` outright.
|
||||
- Dynamic dock columns (`floating.rs`: `Panel::right(Id::new(("dock-column", first)))`) must become
|
||||
`Panel::right(("dock-column", first))`.
|
||||
- Historical planning (pre-#4135): dynamic dock columns in the then-existing app `floating.rs` used `Panel::right(Id::new(("dock-column", first)))` and were to become `Panel::right(("dock-column", first))`. The app `floating` module is now deleted; craft-ui's shared docking owns dock columns and their layout in `panel_docking.rs`.
|
||||
- Do not mechanically wrap an existing `Id` in `IdSalt::new(...)` just to satisfy a signature. Audit the few
|
||||
`make_persistent_id`/`with`/`Atom::custom` sites individually and preserve the intended key with a focus/state
|
||||
test. `ComboBox::from_id_salt` in `widgets::combo`, `Panel::right` in `floating.rs`, and `Atom::custom` in
|
||||
`dialogs/layer_options`/`menus` are the concrete sites to review.
|
||||
test. `ComboBox::from_id_salt` in `widgets::combo`, `Panel::right` in the deleted `floating.rs` (historical), and `Atom::custom` in `dialogs/layer_options`/`menus` were the concrete sites to review.
|
||||
- `Id::NULL` and `Id::with(...)` remain valid.
|
||||
- `Id::unique` also registers debug id sources; do not let debug-only memory growth become a concern.
|
||||
|
||||
### 3.4 Panel identity/persistence
|
||||
### 3.4 Panel identity/persistence (historical planning; superseded by shared docking)
|
||||
|
||||
> **Historical planning (pre-#4135; not current code).** The text below describes the old app-owned `PanelState` width path and is retained as planning evidence. The current tree has none of `seed_dock_panel_width`, `note_dock_width`, `apply_pending_dock_width`, `legacy_dock_default_width`, `shared_dock_default_width` or `dock_width_pending`. Width now persists through `UiState::dock_width`: `panel_docking::main_width` clamps it into the default layout, `default_flags`/`Transaction::commit` writes the resized width back, `workspaces::Workspace` captures and applies it (`saved_workspaces_restore_dock_width`), and `layout_migration` carries it across older files. The layout itself is craft-ui `DockArea`/`craft_ui::docking::Layout`.
|
||||
|
||||
`crates/ui-egui/src/dock.rs::seed_dock_panel_width` and `note_dock_width` read/write
|
||||
`egui::containers::panel::PanelState` under `Id::new(panel)`. In 0.37 `Panel` internally derives its bar id from
|
||||
@@ -456,10 +472,10 @@ historical audit tree `ec8840a` is likewise unsafe. Never merge either tree:
|
||||
group is movable;
|
||||
- `crates/ui/src/docking/surface.rs`: resizing a floating group no longer requires all members movable.
|
||||
- These checks are present in current craft-libs security work (for example #236, docking group move permission). Do not merge that tree.
|
||||
- Keep the egui-related `8614e4a`/`1764bca` shape already in the `3832c47…` base and the uncommitted capture
|
||||
helper, preserving the current permission
|
||||
checks, `craft-platform`, settings, docking, printing and tests. Record the new commit, its parent and the diff.
|
||||
Publish a new immutable final craft-libs revision only after the shared tests and an adversarial review pass.
|
||||
- Keep the egui-related `8614e4a`/`1764bca` shape already in the `3832c47…` base and the CL-2F capture helper
|
||||
(later published in `25da131…`), preserving the permission checks, `craft-platform`, settings, docking, printing
|
||||
and tests. Record the new commit, its parent and the diff. Publish a new immutable final craft-libs revision only
|
||||
after the shared tests and an adversarial review pass.
|
||||
That final revision comes **after** the final reviewed storytold egui/winit revisions (which include E21b and
|
||||
W15P), so no provisional craft-libs commit pretends to be final.
|
||||
- The final craft-libs revision also carries the CL-2F frozen capture-slot helper; VectorCraft's final app lock
|
||||
@@ -679,8 +695,9 @@ craft-window (final craft-libs rev) ── egui 0.37
|
||||
`accesskit 0.25.1`, `accesskit_consumer 0.39.1`, `accesskit_winit 0.34.1`, `accesskit_atspi_common 0.21.0`,
|
||||
`accesskit_unix 0.24.0`, `accesskit_macos 0.27.1`, `accesskit_windows 0.35.1` and `accesskit_ios 0.2.1`; the
|
||||
workspace manifest's requirement strings are lower (`accesskit 0.25.0`, `accesskit_consumer 0.39.0`,
|
||||
`accesskit_winit 0.34.0`). VectorCraft currently locks `kittest 0.4.0`, `accesskit 0.24.1`,
|
||||
`accesskit_winit 0.32.2`. The gate checks resolved registry source, compatible version and a single identity; it
|
||||
`accesskit_winit 0.34.0`). At the pre-migration baseline VectorCraft locked `kittest 0.4.0`, `accesskit 0.24.1`,
|
||||
`accesskit_winit 0.32.2`; the current pins are in the status block above. The gate checks resolved registry source,
|
||||
compatible version and a single identity; it
|
||||
must **not** reject them for not being egui-git packages.
|
||||
- Keep version requirements at `0.37` in `[workspace.dependencies]`, and add root `[patch.crates-io]` entries for
|
||||
every fork-source crate used **and** `winit` to the final immutable revisions. This makes transitive
|
||||
@@ -830,16 +847,12 @@ deadlines and background scheduling. Keep egui's `i.time` for UI animation/gestu
|
||||
- Pass the existing `&mut Ui` from `App::ui` to panels; never call `Context::root_ui` inside `App::ui`, tests that
|
||||
use `run_ui`, or any code that already holds the root. `root_ui` is only for a `Context`-only integration, which
|
||||
VectorCraft does not have.
|
||||
- Keep `egui::Area`/`Window::show(ctx)` for floating panels/dialogs: they create their own `Ui` and do not borrow
|
||||
the root. Do not convert them to `root_ui` calls.
|
||||
- Dialogs keep `egui::Area`/`Window::show(ctx)`: they create their own `Ui` and do not borrow the root. Floating panels are drawn by `craft_ui::docking::DockArea`; on the desktop craft-ui's `native-windows` feature gives them OS windows of their own, and on the web and Wayland they float inside the app window.
|
||||
- Panel order remains app bar / control bar / status+hint / toolbar / dock / central canvas / floating / dialogs.
|
||||
The new `Panel` id-salt behavior must not change the order or clip; assert `response.rect`, `ui.clip_rect()` and
|
||||
hit positions after resize/DPI changes.
|
||||
- VectorCraft has no `show_viewport_deferred`/child native viewport. Do not add one. Screenshot/root behavior is
|
||||
single-viewport; still test two queued screenshot requests (issued one at a time) and hidden-window paint because
|
||||
the callback contract is new.
|
||||
- Docking/floating use `craft_ui::docking` and app `floating`; keep their layout model and permissions. The
|
||||
`craft-ui` rebase must preserve the current `MoveFloating` group permission checks.
|
||||
- VectorCraft does not call `show_viewport_deferred` itself. The only child native viewports are the desktop floating-panel windows that craft-ui's `native-windows` feature creates; the web and Wayland stay in one window. Floating windows' controls are excluded from `ui.docking`'s `hitRects` and from control-channel pointer input (`panel_docking_native_tests.rs`, `docs/control-protocol.md`). Retest screenshot/root behavior for the main viewport with the native child windows present; still test two queued screenshot requests (issued one at a time) and hidden-window paint because the callback contract is new.
|
||||
- Docking/floating use `craft_ui::docking`; the app `floating` module is deleted. `panel_docking::Runtime` keeps only session-only space/raise/focus state, and `panel_docking::valid`/`known` plus `Permissions` preserve the layout model and the `MoveFloating` group permission checks.
|
||||
- Test: panels open/close/reorder, dock width restore, floating panel drag/dock, popup/flyout focus, window resize
|
||||
and 1x/1.5x/2x scale; screenshots and geometry agree.
|
||||
|
||||
@@ -1002,7 +1015,7 @@ Start with the tests that already encode behaviour:
|
||||
| Area | Existing tests/files | What to add |
|
||||
|---|---|---|
|
||||
| Whole-window frame | `crates/ui-egui/src/graphics.rs` `frame`, texture tests | tick/ui split, multi-pass, hidden paint, texture counts |
|
||||
| Panels/dock | `panel_docking.rs`, `panel_docking_tests.rs`, `dock.rs` tests | dock width restore, panel order/clip, 1x/1.5x/2x, floating |
|
||||
| Panels/dock | `panel_docking_tests.rs` (pointer/keyboard docking), `panel_docking_native_tests.rs` (desktop OS windows), `layout_migration_tests.rs` (older layouts), `dock.rs` (2 scroll-bar tests), `workspaces.rs` (saved dock width) | dock width restore, panel order/clip, 1x/1.5x/2x, floating, native-window focus/keyboard/drops, layout conversion |
|
||||
| Tabs/menus | `panels/tabs.rs` tests, `menus.rs` | keyboard/accessibility tree, menu geometry |
|
||||
| Fields | `tests_numfields.rs`, `widgets.rs` tests | wheel, focus, names, roles, click-away selection |
|
||||
| Canvas | `canvas.rs` tests, `tests_rotateview.rs`, `tests_scrub.rs` | wheel source, zoom/pan, multi-pass, IME |
|
||||
@@ -1173,7 +1186,7 @@ time or CLI throughput to UI latency.
|
||||
|
||||
### 6.2 Paired comparison
|
||||
|
||||
- **Performance control:** the pre-migration control is current main `4a7cba0…` (the two-commit DXF delta) with its
|
||||
- **Performance control:** the pre-migration control is the then-current main `4a7cba0…` (the two-commit DXF delta) with its
|
||||
coherent 0.36 lock, not `8906073…` alone. The candidate is the migration branch rebased onto `4a7cba0…` (or a
|
||||
later main after the same gate). This keeps `crates/cad/src/import/curve.rs` identical on both sides and isolates
|
||||
the egui migration. `8906073…` remains the historical planning baseline and rollback identity, not the paired
|
||||
@@ -1297,11 +1310,13 @@ These are not “future work”; they block calling the migration done:
|
||||
P7c, complete). Record exact full SHAs and tree hashes, preserve fork authorship/PATCHES, and prove a clean
|
||||
`cargo fetch --locked`. No intermediate fork stage is a production pin; candidate aiongg revisions remain
|
||||
research inputs; no final SHA is invented in this plan.
|
||||
2. **Final craft-libs revision.** Finish the CL-1/CL-2F delta on the `3832c47…` base (which already carries the
|
||||
selected `8614e4a`/`1764bca` changes), preserving `craft-platform`, security/settings/docking/printing fixes;
|
||||
pin D1b and pass `craft-ui` visual regression. Do not merge PR199 head `b68de8c` or audit tree `ec8840a` as
|
||||
trees. L1 (CL-1) and L2 (CL-2F) precede V2; publish only after shared gates and the capture-helper adapter
|
||||
review.
|
||||
2. **Final craft-libs revision (resolved by the published pin).** The CL-1/CL-2F delta is published in
|
||||
craft-libs `25da131…`; the current workspace pins `craft-ui`, `craft-window`, `craft-keymap` and
|
||||
`craft-print` to descendant `a0916808…`, retaining that delta and upstream shared docking. The plan-time requirement was to finish that delta on the `3832c47…` base (which already
|
||||
carries the selected `8614e4a`/`1764bca` changes), preserving `craft-platform`,
|
||||
security/settings/docking/printing fixes, pin D1b and pass `craft-ui` visual regression. Do not merge PR199
|
||||
head `b68de8c` or audit tree `ec8840a` as trees. L1 (CL-1) and L2 (CL-2F) preceded V2; publication waited for
|
||||
the shared gates and the capture-helper adapter review.
|
||||
3. **`default_fonts`/font provider.** `ui-egui` library tests and no-default-feature builds must bind non-empty
|
||||
families; production `Tofu` would otherwise draw synthetic boxes, and downstream kittest tests do not inherit
|
||||
kittest's own dev-dependency fonts. `system_fonts` remains off unless measured and approved; the shared
|
||||
@@ -1322,7 +1337,7 @@ These are not “future work”; they block calling the migration done:
|
||||
precise char/byte contract and one undo step, plus the E21b model/preedit/UTF-8 payload producer and the
|
||||
complete egui P7b/W27 lifecycle (owner identity, `Available` empty vs `Unavailable`, committed clear boundary,
|
||||
latest-`None` authority, current-epoch mismatch and old-epoch rejection). UI `TextEdit` behavior is separate.
|
||||
8. **Upstream rebase gate.** Current main is `4a7cba0…`, two commits ahead of the pinned review baseline, touching
|
||||
8. **Upstream rebase gate.** At plan time, main was `4a7cba0…`, two commits ahead of the pinned review baseline, touching
|
||||
only `crates/cad/src/import/curve.rs` (DXF spline). Rebase before V2, preserve that fix, rerun its import tests,
|
||||
and isolate it from performance comparisons (§6.2). Recheck if main advanced further.
|
||||
9. **macOS evidence.** No macOS host/display is available here. Keyboard layouts, IME (including press-and-hold),
|
||||
@@ -1399,8 +1414,9 @@ Primary sources inspected (read-only):
|
||||
- `/home/codex/work/CraftApps/egui037-migration-20261022/vectorcraft` (assigned checkout)
|
||||
- `/home/codex/work/CraftApps/egui037-migration-20261022/egui` at `0.37.0` and `aiongg/craft/0.37`
|
||||
- `/home/codex/work/CraftApps/egui037-migration-20261022/winit` at `8b35388…`/`ffae519…`
|
||||
- `/home/codex/work/CraftApps/egui037-migration-20261022/craft-libs-implementation` at `3832c47…` (the local
|
||||
implementation base, with the uncommitted CL-2F helper) and the historical PR 199/audit branches
|
||||
- `/home/codex/work/CraftApps/egui037-migration-20261022/craft-libs-implementation` at `3832c47…` (the historical
|
||||
local planning base; the CL-2F helper later landed in published craft-libs `25da131…`) and the historical PR
|
||||
199/audit branches
|
||||
- `craft-libs/docs/egui-037-suite-rollout.md` and `craft-libs/docs/egui-037-migration-plan.md`
|
||||
- `egui/docs/egui-037-migration-plan.md` (E21b, W15P, family pins, §10.6 font fixtures)
|
||||
- `/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/` (upstream discussions, text-input research,
|
||||
@@ -1478,7 +1494,9 @@ These are decisions this plan recommends but does not silently make:
|
||||
| 2026-10-23 | Initial source-grounded planning draft. No implementation, build, test or GUI run. |
|
||||
| 2026-10-23 | Revision 2: resolved every `vectorcraft-v1` finding (§8.1); corrected the lifecycle model (`raw_input_hook` in both hosts, separate host/accepted-frame counters, host-ordered test driver); added E21b and W15P/E4 prerequisites; moved the weak capture slot to the shared craft-ui contract; fixed final-fork → final craft-libs/CL-2F → app ordering; enumerated family and external registry identities; corrected `row_label` and the `Id::new` inventory; recorded the pinned review baseline, `4a7cba0…` drift/rebase gate and same-engine performance control. Static documentation only; no build/test run. |
|
||||
| 2026-10-23 | Revision 3: resolved every `vectorcraft-v2` finding (§8.2) by replacing contradictory prose: stock-0.37 third arm in visual/performance; shared 4/128/256 MiB capture admission with epoch-bound geometry and hidden paint request; W27 old-epoch invalidation and complete E21b contract; P7c owns #8763/#8748; corrected production `Tofu` vs kittest `Panic` and non-propagating kittest dev-dependency fonts; craft-ui exact-pixel visual regressions; split `ui-deps` ledger; split D1a winit / D1b egui P8; per-stage rollback; removed the dead provisional capture slot and app-local adapter; corrected clipboard scope; added W15P synchronous claims and bounded transfers; set 5% median/10% p95/5% RSS gates with no tolerance widening. Static documentation only; no build/test run. |
|
||||
| 2026-10-24 | Revision 4: implementation status; repaired the final upstream-review defects (path+data capture downgrade, image-paste repaint and paste-time chord, monotonic hidden tick, IME context invalidation and unavailable revisions, per-viewport exact-once input, Artboard and Split Grid accessible names, fail-closed stale synthetic input); moved the shared-library base reference to the local `3832c47…` implementation base. The exact tree still needs fresh managed verification before publication. |
|
||||
| 2026-10-24 | Revision 4: implementation status; repaired the final upstream-review defects (path+data capture downgrade, image-paste repaint and paste-time chord, monotonic hidden tick, IME context invalidation and unavailable revisions, per-viewport exact-once input, Artboard and Split Grid accessible names, fail-closed stale synthetic input); moved the shared-library base reference to the local `3832c47…` implementation base. At that revision the exact tree still needed fresh managed verification before publication. |
|
||||
| 2026-10-25 | Documentation reconciliation after PR 4113 and managed job `egui037-vector-postmerge-ci-20261025-1381`: recorded the current merge tree and pass result, marked the `8bf090b…`/`0589` status and 0.36.2 lock table historical, listed current pins from `Cargo.toml`/`Cargo.lock`, and corrected the plan-time local CL-2F base description to the published craft-libs `25da131…`. Documentation only; no build/test run. |
|
||||
| 2026-10-25 | Shared-docking reconciliation: recorded the local merge of upstream `main` `714434527…` (PR #4135), the upstream/delete resolution of the four conflicts, the reviewed recolor code already in `main`, and CI 1422's pre-merge pass; corrected current docking claims (desktop OS windows, web/Wayland in-page, deleted `floating.rs` and width helpers, `UiState::dock_width` persistence, new native and layout-migration tests) while retaining the earlier planning sections as historical. The combined executable source and lockfile were subsequently qualified by managed job `egui037-vector-combined-ci1443` (all 12 stages passed); the status text was updated after that run. |
|
||||
|
||||
|
||||
## Local implementation update — 2026-10-23
|
||||
|
||||
Reference in New Issue
Block a user